Skip to main content

flodl_cli/
builtins.rs

1//! Registry of built-in commands. Single source of truth for dispatch,
2//! help listing, collision detection, and shell completion.
3//!
4//! Each leaf sub-command owns a `#[derive(FdlArgs)]` struct that carries
5//! the canonical flag set. `BuiltinSpec::schema_fn` returns the
6//! `Schema` derived from that struct, so completion rules
7//! (`--cuda <TAB>` → `12.6 12.8`, etc.) flow through the same pipeline
8//! as project commands rather than a hand-mirrored flag table.
9
10use crate::args::FdlArgsTrait;
11use crate::config::Schema;
12
13// ---------------------------------------------------------------------------
14// FdlArgs structs (one per leaf sub-command)
15//
16// These dogfood the derive macro across flodl-cli itself. Each is parsed
17// with `parse_or_schema_from(&argv)` from a sliced argv tail; the derive
18// handles argv, `--help`, and `--fdl-schema` uniformly.
19// ---------------------------------------------------------------------------
20
21/// Interactive guided setup wizard.
22#[derive(crate::FdlArgs, Debug)]
23pub struct SetupArgs {
24    /// Skip all prompts and use auto-detected defaults.
25    #[option(short = 'y')]
26    pub non_interactive: bool,
27    /// Re-download or rebuild even if libtorch exists.
28    #[option]
29    pub force: bool,
30}
31
32/// System and GPU diagnostics.
33#[derive(crate::FdlArgs, Debug)]
34pub struct DiagnoseArgs {
35    /// Emit machine-readable JSON.
36    #[option]
37    pub json: bool,
38}
39
40/// Generate flodl API reference.
41#[derive(crate::FdlArgs, Debug)]
42pub struct ApiRefArgs {
43    /// Emit machine-readable JSON.
44    #[option]
45    pub json: bool,
46    /// Explicit flodl source path (defaults to detected project root).
47    #[option]
48    pub path: Option<String>,
49}
50
51/// Scaffold a new floDl project.
52///
53/// Three modes, mutually exclusive:
54///   default (no flag) — Docker with host-mounted libtorch (recommended)
55///   --docker          — Docker with libtorch baked into the image
56///   --native          — no Docker, host-provided libtorch + cargo
57#[derive(crate::FdlArgs, Debug)]
58pub struct InitArgs {
59    /// New project directory name.
60    #[arg]
61    pub name: Option<String>,
62    /// Generate a Docker scaffold with libtorch baked into the image.
63    #[option]
64    pub docker: bool,
65    /// Generate a native scaffold (no Docker; libtorch provided on the host).
66    #[option]
67    pub native: bool,
68    /// Also scaffold the flodl-hf HuggingFace playground (skips the prompt).
69    #[option]
70    pub with_hf: bool,
71}
72
73/// Add a flodl ecosystem crate as a side playground inside the current
74/// flodl project.
75///
76/// Currently supports `flodl-hf` (alias: `hf`). Drops a standalone
77/// cargo crate under `./flodl-hf/` with pinned deps, a one-file
78/// AutoModel example, `fdl.yml` with runnable commands, and a README
79/// documenting feature flavors and next steps. Does NOT mutate the
80/// caller's root `Cargo.toml` or `fdl.yml`.
81#[derive(crate::FdlArgs, Debug)]
82pub struct AddArgs {
83    /// Target to scaffold (currently: `flodl-hf` or the alias `hf`).
84    #[arg]
85    pub target: Option<String>,
86}
87
88/// Install or update fdl globally (~/.local/bin/fdl).
89#[derive(crate::FdlArgs, Debug)]
90pub struct InstallArgs {
91    /// Check for updates without installing.
92    #[option]
93    pub check: bool,
94    /// Symlink to the current binary (tracks local builds).
95    #[option]
96    pub dev: bool,
97}
98
99/// List installed libtorch variants.
100#[derive(crate::FdlArgs, Debug)]
101pub struct LibtorchListArgs {
102    /// Emit machine-readable JSON.
103    #[option]
104    pub json: bool,
105}
106
107/// Activate a libtorch variant.
108#[derive(crate::FdlArgs, Debug)]
109pub struct LibtorchActivateArgs {
110    /// Variant to activate (as shown by `fdl libtorch list`).
111    #[arg]
112    pub variant: Option<String>,
113}
114
115/// Remove a libtorch variant.
116#[derive(crate::FdlArgs, Debug)]
117pub struct LibtorchRemoveArgs {
118    /// Variant to remove (as shown by `fdl libtorch list`).
119    #[arg]
120    pub variant: Option<String>,
121}
122
123/// Download a pre-built libtorch variant.
124#[derive(crate::FdlArgs, Debug)]
125pub struct LibtorchDownloadArgs {
126    /// Force the CPU variant.
127    #[option]
128    pub cpu: bool,
129    /// Pick a specific CUDA version (instead of auto-detect).
130    #[option(choices = &["12.6", "12.8"])]
131    pub cuda: Option<String>,
132    /// Install libtorch to this directory (default: project libtorch/).
133    #[option]
134    pub path: Option<String>,
135    /// Do not activate after download.
136    #[option]
137    pub no_activate: bool,
138    /// Show what would happen without downloading.
139    #[option]
140    pub dry_run: bool,
141}
142
143/// Build libtorch from source.
144#[derive(crate::FdlArgs, Debug)]
145pub struct LibtorchBuildArgs {
146    /// Override CUDA architectures (semicolon-separated, e.g. "6.1;12.0").
147    #[option]
148    pub archs: Option<String>,
149    /// Parallel compilation jobs.
150    #[option(default = "6")]
151    pub jobs: usize,
152    /// Force Docker build (isolated, reproducible).
153    #[option]
154    pub docker: bool,
155    /// Force native build (faster, requires host toolchain).
156    #[option]
157    pub native: bool,
158    /// Show what would happen without building.
159    #[option]
160    pub dry_run: bool,
161}
162
163/// Install AI coding assistant skills.
164#[derive(crate::FdlArgs, Debug)]
165pub struct SkillInstallArgs {
166    /// Target tool (defaults to auto-detect).
167    #[option]
168    pub tool: Option<String>,
169    /// Specific skill name (defaults to all detected skills).
170    #[option]
171    pub skill: Option<String>,
172}
173
174/// List cached `--fdl-schema` outputs.
175#[derive(crate::FdlArgs, Debug)]
176pub struct SchemaListArgs {
177    /// Emit machine-readable JSON.
178    #[option]
179    pub json: bool,
180}
181
182/// Clear cached schemas. No command name clears all.
183#[derive(crate::FdlArgs, Debug)]
184pub struct SchemaClearArgs {
185    /// Command name to clear (defaults to all).
186    #[arg]
187    pub cmd: Option<String>,
188}
189
190/// Re-probe each entry and rewrite the cache.
191#[derive(crate::FdlArgs, Debug)]
192pub struct SchemaRefreshArgs {
193    /// Command name to refresh (defaults to all).
194    #[arg]
195    pub cmd: Option<String>,
196}
197
198// ---------------------------------------------------------------------------
199// Registry
200// ---------------------------------------------------------------------------
201
202/// One built-in command (or sub-command) slot.
203pub struct BuiltinSpec {
204    /// Path from the top-level command name. `["install"]`,
205    /// `["libtorch", "download"]`.
206    pub path: &'static [&'static str],
207    /// One-line description for `fdl -h` listing. `None` = hidden
208    /// (reserved for collision detection but not shown in help).
209    pub description: Option<&'static str>,
210    /// Constructor for the command's schema. `None` for parent commands
211    /// that only group sub-commands (e.g. `libtorch` itself has no args)
212    /// or for leaves whose argv is parsed by hand (`config show`,
213    /// `completions`, `autocomplete`).
214    pub schema_fn: Option<fn() -> Schema>,
215}
216
217/// Ordered registry of every built-in. Order drives `fdl -h` and the
218/// top-level completion word list, so it mirrors today's `BUILTINS`
219/// const in `main.rs`.
220pub fn registry() -> &'static [BuiltinSpec] {
221    static REG: &[BuiltinSpec] = &[
222        BuiltinSpec {
223            path: &["setup"],
224            description: Some("Interactive guided setup"),
225            schema_fn: Some(SetupArgs::schema),
226        },
227        BuiltinSpec {
228            path: &["libtorch"],
229            description: Some("Manage libtorch installations"),
230            schema_fn: None,
231        },
232        BuiltinSpec {
233            path: &["libtorch", "download"],
234            description: Some("Download pre-built libtorch"),
235            schema_fn: Some(LibtorchDownloadArgs::schema),
236        },
237        BuiltinSpec {
238            path: &["libtorch", "build"],
239            description: Some("Build libtorch from source"),
240            schema_fn: Some(LibtorchBuildArgs::schema),
241        },
242        BuiltinSpec {
243            path: &["libtorch", "list"],
244            description: Some("Show installed variants"),
245            schema_fn: Some(LibtorchListArgs::schema),
246        },
247        BuiltinSpec {
248            path: &["libtorch", "activate"],
249            description: Some("Set active variant"),
250            schema_fn: Some(LibtorchActivateArgs::schema),
251        },
252        BuiltinSpec {
253            path: &["libtorch", "remove"],
254            description: Some("Remove a variant"),
255            schema_fn: Some(LibtorchRemoveArgs::schema),
256        },
257        BuiltinSpec {
258            path: &["libtorch", "info"],
259            description: Some("Show active variant details"),
260            schema_fn: None,
261        },
262        BuiltinSpec {
263            path: &["init"],
264            description: Some("Scaffold a new floDl project"),
265            schema_fn: Some(InitArgs::schema),
266        },
267        BuiltinSpec {
268            path: &["add"],
269            description: Some("Add a flodl ecosystem crate (currently: flodl-hf)"),
270            schema_fn: Some(AddArgs::schema),
271        },
272        BuiltinSpec {
273            path: &["diagnose"],
274            description: Some("System and GPU diagnostics"),
275            schema_fn: Some(DiagnoseArgs::schema),
276        },
277        BuiltinSpec {
278            path: &["install"],
279            description: Some("Install or update fdl globally"),
280            schema_fn: Some(InstallArgs::schema),
281        },
282        BuiltinSpec {
283            path: &["skill"],
284            description: Some("Manage AI coding assistant skills"),
285            schema_fn: None,
286        },
287        BuiltinSpec {
288            path: &["skill", "install"],
289            description: Some("Install skills for the detected tool"),
290            schema_fn: Some(SkillInstallArgs::schema),
291        },
292        BuiltinSpec {
293            path: &["skill", "list"],
294            description: Some("Show available skills"),
295            schema_fn: None,
296        },
297        BuiltinSpec {
298            path: &["api-ref"],
299            description: Some("Generate flodl API reference"),
300            schema_fn: Some(ApiRefArgs::schema),
301        },
302        BuiltinSpec {
303            path: &["config"],
304            description: Some("Inspect resolved project configuration"),
305            schema_fn: None,
306        },
307        BuiltinSpec {
308            path: &["config", "show"],
309            description: Some("Print the resolved merged config"),
310            schema_fn: None,
311        },
312        BuiltinSpec {
313            path: &["schema"],
314            description: Some("Inspect, clear, or refresh cached --fdl-schema outputs"),
315            schema_fn: None,
316        },
317        BuiltinSpec {
318            path: &["schema", "list"],
319            description: Some("Show every cached schema with status"),
320            schema_fn: Some(SchemaListArgs::schema),
321        },
322        BuiltinSpec {
323            path: &["schema", "clear"],
324            description: Some("Delete cached schema(s)"),
325            schema_fn: Some(SchemaClearArgs::schema),
326        },
327        BuiltinSpec {
328            path: &["schema", "refresh"],
329            description: Some("Re-probe each entry and rewrite the cache"),
330            schema_fn: Some(SchemaRefreshArgs::schema),
331        },
332        BuiltinSpec {
333            path: &["completions"],
334            description: Some("Emit shell completion script (bash|zsh|fish)"),
335            schema_fn: None,
336        },
337        BuiltinSpec {
338            path: &["autocomplete"],
339            description: Some("Install completions into the detected shell"),
340            schema_fn: None,
341        },
342        // Hidden: `version` is covered by `-V` / `--version` but still
343        // reserved so first-arg env detection doesn't hijack it.
344        BuiltinSpec {
345            path: &["version"],
346            description: None,
347            schema_fn: None,
348        },
349    ];
350    REG
351}
352
353/// True when `name` is a reserved top-level built-in (visible or hidden).
354/// Drives env-collision detection in first-arg resolution.
355pub fn is_builtin_name(name: &str) -> bool {
356    registry()
357        .iter()
358        .any(|s| s.path.len() == 1 && s.path[0] == name)
359}
360
361/// Visible top-level built-ins as `(name, description)` pairs, in
362/// registry order. Feeds `run::print_project_help` and the fallback
363/// `print_usage`.
364pub fn visible_top_level() -> Vec<(&'static str, &'static str)> {
365    registry()
366        .iter()
367        .filter(|s| s.path.len() == 1)
368        .filter_map(|s| s.description.map(|d| (s.path[0], d)))
369        .collect()
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use std::collections::HashSet;
376
377
378
379    #[test]
380    fn registry_has_no_duplicate_paths() {
381        let mut seen = HashSet::new();
382        for s in registry() {
383            let key = s.path.join(" ");
384            assert!(
385                seen.insert(key.clone()),
386                "duplicate registry path: {key}"
387            );
388        }
389    }
390
391    #[test]
392    fn hidden_entries_have_no_description() {
393        for s in registry() {
394            if s.path == ["version"] {
395                assert!(s.description.is_none(),
396                    "`version` is hidden but carries a description");
397            }
398        }
399    }
400
401    #[test]
402    fn every_parent_has_at_least_one_child() {
403        let parents: HashSet<&str> = registry()
404            .iter()
405            .filter(|s| s.path.len() == 1 && s.schema_fn.is_none()
406                && s.description.is_some())
407            .map(|s| s.path[0])
408            .collect();
409
410        // `completions`, `autocomplete` are leaves with no schema — exclude
411        // them by checking that parents have at least one 2-path child.
412        for parent in &parents {
413            let has_child = registry().iter().any(|s| s.path.len() == 2 && s.path[0] == *parent);
414            if !has_child {
415                // `completions` / `autocomplete` / `version` end up here by
416                // virtue of having no children; they are leaf built-ins.
417                continue;
418            }
419            assert!(has_child, "parent `{parent}` has no child entries");
420        }
421    }
422
423    #[test]
424    fn top_level_dispatched_by_main_is_in_registry() {
425        // Compile-time guard: every match arm target in main.rs is listed
426        // here. Keeping the list local (rather than introspecting main.rs)
427        // documents the coupling explicitly.
428        let dispatched = [
429            "setup", "libtorch", "diagnose", "api-ref", "init", "add",
430            "install", "skill", "schema", "completions", "autocomplete",
431            "config", "version",
432        ];
433        for name in &dispatched {
434            assert!(
435                is_builtin_name(name),
436                "`{name}` dispatched by main.rs but missing from registry"
437            );
438        }
439    }
440
441    #[test]
442    fn visible_top_level_matches_help_ordering() {
443        let top = visible_top_level();
444        let names: Vec<&str> = top.iter().map(|(n, _)| *n).collect();
445        // Lock in the order that `fdl -h` depends on.
446        assert_eq!(
447            names,
448            vec![
449                "setup", "libtorch", "init", "add", "diagnose", "install",
450                "skill", "api-ref", "config", "schema", "completions",
451                "autocomplete",
452            ]
453        );
454    }
455
456    #[test]
457    fn libtorch_download_schema_carries_cuda_choices() {
458        let spec = registry()
459            .iter()
460            .find(|s| s.path == ["libtorch", "download"])
461            .expect("libtorch download entry present");
462        let schema = (spec.schema_fn.expect("download has schema"))();
463        let cuda = schema
464            .options
465            .get("cuda")
466            .expect("`--cuda` option declared");
467        let choices = cuda.choices.as_ref().expect("--cuda has choices");
468        let values: Vec<String> = choices
469            .iter()
470            .filter_map(|v| v.as_str().map(str::to_string))
471            .collect();
472        assert_eq!(values, vec!["12.6".to_string(), "12.8".into()]);
473    }
474}