1use crate::args::FdlArgsTrait;
11use crate::config::Schema;
12
13#[derive(crate::FdlArgs, Debug)]
23pub struct SetupArgs {
24 #[option(short = 'y')]
26 pub non_interactive: bool,
27 #[option]
29 pub force: bool,
30}
31
32#[derive(crate::FdlArgs, Debug)]
34pub struct DiagnoseArgs {
35 #[option]
37 pub json: bool,
38}
39
40#[derive(crate::FdlArgs, Debug)]
42pub struct ApiRefArgs {
43 #[option]
45 pub json: bool,
46 #[option]
48 pub path: Option<String>,
49}
50
51#[derive(crate::FdlArgs, Debug)]
58pub struct InitArgs {
59 #[arg]
61 pub name: Option<String>,
62 #[option]
64 pub docker: bool,
65 #[option]
67 pub native: bool,
68}
69
70#[derive(crate::FdlArgs, Debug)]
72pub struct InstallArgs {
73 #[option]
75 pub check: bool,
76 #[option]
78 pub dev: bool,
79}
80
81#[derive(crate::FdlArgs, Debug)]
83pub struct LibtorchListArgs {
84 #[option]
86 pub json: bool,
87}
88
89#[derive(crate::FdlArgs, Debug)]
91pub struct LibtorchActivateArgs {
92 #[arg]
94 pub variant: Option<String>,
95}
96
97#[derive(crate::FdlArgs, Debug)]
99pub struct LibtorchRemoveArgs {
100 #[arg]
102 pub variant: Option<String>,
103}
104
105#[derive(crate::FdlArgs, Debug)]
107pub struct LibtorchDownloadArgs {
108 #[option]
110 pub cpu: bool,
111 #[option(choices = &["12.6", "12.8"])]
113 pub cuda: Option<String>,
114 #[option]
116 pub path: Option<String>,
117 #[option]
119 pub no_activate: bool,
120 #[option]
122 pub dry_run: bool,
123}
124
125#[derive(crate::FdlArgs, Debug)]
127pub struct LibtorchBuildArgs {
128 #[option]
130 pub archs: Option<String>,
131 #[option(default = "6")]
133 pub jobs: usize,
134 #[option]
136 pub docker: bool,
137 #[option]
139 pub native: bool,
140 #[option]
142 pub dry_run: bool,
143}
144
145#[derive(crate::FdlArgs, Debug)]
147pub struct SkillInstallArgs {
148 #[option]
150 pub tool: Option<String>,
151 #[option]
153 pub skill: Option<String>,
154}
155
156#[derive(crate::FdlArgs, Debug)]
158pub struct SchemaListArgs {
159 #[option]
161 pub json: bool,
162}
163
164#[derive(crate::FdlArgs, Debug)]
166pub struct SchemaClearArgs {
167 #[arg]
169 pub cmd: Option<String>,
170}
171
172#[derive(crate::FdlArgs, Debug)]
174pub struct SchemaRefreshArgs {
175 #[arg]
177 pub cmd: Option<String>,
178}
179
180pub struct BuiltinSpec {
186 pub path: &'static [&'static str],
189 pub description: Option<&'static str>,
192 pub schema_fn: Option<fn() -> Schema>,
197}
198
199pub fn registry() -> &'static [BuiltinSpec] {
203 static REG: &[BuiltinSpec] = &[
204 BuiltinSpec {
205 path: &["setup"],
206 description: Some("Interactive guided setup"),
207 schema_fn: Some(SetupArgs::schema),
208 },
209 BuiltinSpec {
210 path: &["libtorch"],
211 description: Some("Manage libtorch installations"),
212 schema_fn: None,
213 },
214 BuiltinSpec {
215 path: &["libtorch", "download"],
216 description: Some("Download pre-built libtorch"),
217 schema_fn: Some(LibtorchDownloadArgs::schema),
218 },
219 BuiltinSpec {
220 path: &["libtorch", "build"],
221 description: Some("Build libtorch from source"),
222 schema_fn: Some(LibtorchBuildArgs::schema),
223 },
224 BuiltinSpec {
225 path: &["libtorch", "list"],
226 description: Some("Show installed variants"),
227 schema_fn: Some(LibtorchListArgs::schema),
228 },
229 BuiltinSpec {
230 path: &["libtorch", "activate"],
231 description: Some("Set active variant"),
232 schema_fn: Some(LibtorchActivateArgs::schema),
233 },
234 BuiltinSpec {
235 path: &["libtorch", "remove"],
236 description: Some("Remove a variant"),
237 schema_fn: Some(LibtorchRemoveArgs::schema),
238 },
239 BuiltinSpec {
240 path: &["libtorch", "info"],
241 description: Some("Show active variant details"),
242 schema_fn: None,
243 },
244 BuiltinSpec {
245 path: &["init"],
246 description: Some("Scaffold a new floDl project"),
247 schema_fn: Some(InitArgs::schema),
248 },
249 BuiltinSpec {
250 path: &["diagnose"],
251 description: Some("System and GPU diagnostics"),
252 schema_fn: Some(DiagnoseArgs::schema),
253 },
254 BuiltinSpec {
255 path: &["install"],
256 description: Some("Install or update fdl globally"),
257 schema_fn: Some(InstallArgs::schema),
258 },
259 BuiltinSpec {
260 path: &["skill"],
261 description: Some("Manage AI coding assistant skills"),
262 schema_fn: None,
263 },
264 BuiltinSpec {
265 path: &["skill", "install"],
266 description: Some("Install skills for the detected tool"),
267 schema_fn: Some(SkillInstallArgs::schema),
268 },
269 BuiltinSpec {
270 path: &["skill", "list"],
271 description: Some("Show available skills"),
272 schema_fn: None,
273 },
274 BuiltinSpec {
275 path: &["api-ref"],
276 description: Some("Generate flodl API reference"),
277 schema_fn: Some(ApiRefArgs::schema),
278 },
279 BuiltinSpec {
280 path: &["config"],
281 description: Some("Inspect resolved project configuration"),
282 schema_fn: None,
283 },
284 BuiltinSpec {
285 path: &["config", "show"],
286 description: Some("Print the resolved merged config"),
287 schema_fn: None,
288 },
289 BuiltinSpec {
290 path: &["schema"],
291 description: Some("Inspect, clear, or refresh cached --fdl-schema outputs"),
292 schema_fn: None,
293 },
294 BuiltinSpec {
295 path: &["schema", "list"],
296 description: Some("Show every cached schema with status"),
297 schema_fn: Some(SchemaListArgs::schema),
298 },
299 BuiltinSpec {
300 path: &["schema", "clear"],
301 description: Some("Delete cached schema(s)"),
302 schema_fn: Some(SchemaClearArgs::schema),
303 },
304 BuiltinSpec {
305 path: &["schema", "refresh"],
306 description: Some("Re-probe each entry and rewrite the cache"),
307 schema_fn: Some(SchemaRefreshArgs::schema),
308 },
309 BuiltinSpec {
310 path: &["completions"],
311 description: Some("Emit shell completion script (bash|zsh|fish)"),
312 schema_fn: None,
313 },
314 BuiltinSpec {
315 path: &["autocomplete"],
316 description: Some("Install completions into the detected shell"),
317 schema_fn: None,
318 },
319 BuiltinSpec {
322 path: &["version"],
323 description: None,
324 schema_fn: None,
325 },
326 ];
327 REG
328}
329
330pub fn is_builtin_name(name: &str) -> bool {
333 registry()
334 .iter()
335 .any(|s| s.path.len() == 1 && s.path[0] == name)
336}
337
338pub fn visible_top_level() -> Vec<(&'static str, &'static str)> {
342 registry()
343 .iter()
344 .filter(|s| s.path.len() == 1)
345 .filter_map(|s| s.description.map(|d| (s.path[0], d)))
346 .collect()
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use std::collections::HashSet;
353
354
355
356 #[test]
357 fn registry_has_no_duplicate_paths() {
358 let mut seen = HashSet::new();
359 for s in registry() {
360 let key = s.path.join(" ");
361 assert!(
362 seen.insert(key.clone()),
363 "duplicate registry path: {key}"
364 );
365 }
366 }
367
368 #[test]
369 fn hidden_entries_have_no_description() {
370 for s in registry() {
371 if s.path == ["version"] {
372 assert!(s.description.is_none(),
373 "`version` is hidden but carries a description");
374 }
375 }
376 }
377
378 #[test]
379 fn every_parent_has_at_least_one_child() {
380 let parents: HashSet<&str> = registry()
381 .iter()
382 .filter(|s| s.path.len() == 1 && s.schema_fn.is_none()
383 && s.description.is_some())
384 .map(|s| s.path[0])
385 .collect();
386
387 for parent in &parents {
390 let has_child = registry().iter().any(|s| s.path.len() == 2 && s.path[0] == *parent);
391 if !has_child {
392 continue;
395 }
396 assert!(has_child, "parent `{parent}` has no child entries");
397 }
398 }
399
400 #[test]
401 fn top_level_dispatched_by_main_is_in_registry() {
402 let dispatched = [
406 "setup", "libtorch", "diagnose", "api-ref", "init", "install",
407 "skill", "schema", "completions", "autocomplete", "config",
408 "version",
409 ];
410 for name in &dispatched {
411 assert!(
412 is_builtin_name(name),
413 "`{name}` dispatched by main.rs but missing from registry"
414 );
415 }
416 }
417
418 #[test]
419 fn visible_top_level_matches_help_ordering() {
420 let top = visible_top_level();
421 let names: Vec<&str> = top.iter().map(|(n, _)| *n).collect();
422 assert_eq!(
424 names,
425 vec![
426 "setup", "libtorch", "init", "diagnose", "install", "skill",
427 "api-ref", "config", "schema", "completions", "autocomplete",
428 ]
429 );
430 }
431
432 #[test]
433 fn libtorch_download_schema_carries_cuda_choices() {
434 let spec = registry()
435 .iter()
436 .find(|s| s.path == ["libtorch", "download"])
437 .expect("libtorch download entry present");
438 let schema = (spec.schema_fn.expect("download has schema"))();
439 let cuda = schema
440 .options
441 .get("cuda")
442 .expect("`--cuda` option declared");
443 let choices = cuda.choices.as_ref().expect("--cuda has choices");
444 let values: Vec<String> = choices
445 .iter()
446 .filter_map(|v| v.as_str().map(str::to_string))
447 .collect();
448 assert_eq!(values, vec!["12.6".to_string(), "12.8".into()]);
449 }
450}