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/// Cluster readiness probe.
41///
42/// Default (single-host): probes the local box for GPU + libtorch arch
43/// match + shared-data path + NCCL availability. Cluster context
44/// (`fdl @cluster probe` / `FDL_ENV=cluster`): probes every host in
45/// `fdl.cluster.yml` via SSH and aggregates the report.
46///
47/// Exit code: 0 when every checked component is green; 1 when any
48/// issue was surfaced. `fdl deploy` and CI consume the `--json` shape.
49#[derive(crate::FdlArgs, Debug)]
50pub struct ProbeArgs {
51 /// Emit machine-readable JSON.
52 #[option]
53 pub json: bool,
54 /// Skip the shared-data-mount visibility check. Useful for
55 /// single-host setups without a shared filesystem configured.
56 #[option]
57 pub skip_mount: bool,
58 /// Override the shared-data path (default: cluster.yml's
59 /// per-host `data_path:`, or the convention default
60 /// `/flodl/data` when unset).
61 #[option]
62 pub data_path: Option<std::path::PathBuf>,
63 /// Override the libtorch directory. Default walks up from cwd
64 /// for `libtorch/.active`. Use this when the libtorch install
65 /// lives outside the project tree (e.g. a separate virtiofs
66 /// share mounted at a known path on a worker node).
67 #[option]
68 pub libtorch_path: Option<std::path::PathBuf>,
69 /// Treat NCCL as provided by a Docker image (compose service name
70 /// from `fdl.yml`, e.g. `cuda`). Suppresses host-level NCCL
71 /// discovery and reports "via Docker image `<svc>`" instead. In
72 /// cluster mode, this is auto-derived from each host's `docker:`
73 /// field in `fdl.cluster.yml`.
74 #[option]
75 pub docker: Option<String>,
76}
77
78/// Live cluster run status.
79///
80/// Fetches the controller's `state.json` (membership + lifecycle
81/// phase, served on the training port itself) and pretty-prints it.
82/// Live for the whole run, join window included: shows who has joined
83/// while the world is still forming.
84///
85/// Exit code: 0 when the state was fetched; 1 when no endpoint
86/// answered (usually: no run is up).
87#[derive(crate::FdlArgs, Debug)]
88pub struct StatusArgs {
89 /// Emit the raw state.json body instead of the human summary.
90 #[option]
91 pub json: bool,
92 /// Controller address to query, `host[:port]` (default port 1337).
93 /// Overrides the active env's `cluster.controller`. This is all a
94 /// self-deployed worker's operator needs to watch a run.
95 #[option]
96 pub addr: Option<String>,
97}
98
99/// Fire the operator start switch of a staging cluster run.
100///
101/// A join window opened with `controller.join.start: manual` (or
102/// `hybrid`) holds the roster once quorum is met instead of forming on
103/// the clock — inspect it with `fdl status`, then fire the topology
104/// freeze with this command. Refusals name their reason (auto mode,
105/// quorum not met, window already closed).
106///
107/// Trust mirrors join admission: fired from the controller host (or
108/// through the sshd tunnel) no credential is needed; from anywhere
109/// else pass `--token` (the run's `controller.join.token`).
110///
111/// Exit code: 0 when the start was armed; 1 otherwise.
112#[derive(crate::FdlArgs, Debug)]
113pub struct StartArgs {
114 /// Controller address, `host[:port]` (default port 1337).
115 /// Overrides the active env's `cluster.controller`.
116 #[option]
117 pub addr: Option<String>,
118 /// Session credential for a non-loopback fire (the run's
119 /// `controller.join.token`).
120 #[option]
121 pub token: Option<String>,
122}
123
124/// Join a cluster run's window as a self-deployed worker.
125///
126/// Dials the controller's join channel, offers this box's GPUs, and
127/// runs the training binary (`--bin`) in agent role: the binary joins,
128/// then spawns and supervises this host's relay and rank children
129/// itself. Every flag defaults from the `join:` block of fdl.yml when
130/// present; flags win. Arguments after a standalone `--` go to the
131/// training binary verbatim (they must match the run — rank children
132/// re-enter the binary with them).
133///
134/// Trust, mirroring join admission: `--token` presents the run's
135/// pre-shared credential; `--ssh` reaches a loopback-bound controller
136/// through its guardrailed sshd (reachability = authentication); with
137/// neither, the controller must run open admission.
138///
139/// Before dialing, the box is prepared: the GPU stack is checked, the
140/// dataset source root is put where the ranks will look for it
141/// (`--data-source` mounts it read-only when it is not already there),
142/// the directories the data plane writes are proven writable, and
143/// anything this box does not have yet is acquired — a libtorch variant
144/// (`--libtorch`) and the training binary itself (`--source`, fetched to
145/// local disk and built here, which is what makes its ABI match the
146/// libtorch it holds). Preparation re-runs per attempt, so a `--persist`
147/// box picks up a changed source on its next re-dial.
148///
149/// Exit code: the agent's exit code (0 = this host finished cleanly);
150/// 2 for a failure retrying cannot fix (no GPU, a spec that does not
151/// parse, a missing binary or toolchain), which `--persist` does not
152/// re-dial; 1 for a transient one, which it does — the systemd /
153/// golden-image mode. Source that does not compile counts as transient
154/// on purpose: the fix is a push away at the source, and a box that
155/// stopped permanently over a typo would be powered off by the systemd
156/// recipe. Exception: a compile failure with the vendor toolkit headers
157/// missing is permanent, since re-dialing cannot install a package.
158#[derive(crate::FdlArgs, Debug)]
159pub struct JoinArgs {
160 /// Controller mux address, `host[:port]` (default port 1337).
161 /// With `--ssh`, the address as seen FROM the ssh host — the
162 /// default `127.0.0.1:1337` is the sshd-on-the-controller-box
163 /// convention.
164 #[arg]
165 pub controller: Option<String>,
166 /// SSH tunnel hop, `[user@]host[:port]`: brings up a local `-L`
167 /// forward of the controller port and dials through it.
168 #[option]
169 pub ssh: Option<String>,
170 /// Identity file for the tunnel (`ssh -i`).
171 #[option]
172 pub identity: Option<String>,
173 /// Pre-shared session credential (the run's
174 /// `controller.join.token`).
175 #[option]
176 pub token: Option<String>,
177 /// Training binary to run in agent role, as a path on this box.
178 /// Mutually exclusive with `--source`.
179 #[option]
180 pub bin: Option<String>,
181 /// Build the training binary here instead: a source spec, one of
182 /// `file:///abs/path`, `rsync://[user@]host[:port]:/abs/path` or
183 /// `git+https://host/owner/repo#<tag|branch|sha>`. Fetched to local
184 /// disk, then built against this box's libtorch.
185 #[option]
186 pub source: Option<String>,
187 /// Project directory inside the fetched source tree (default: its
188 /// root). Governs the build and the run both.
189 #[option]
190 pub source_cwd: Option<String>,
191 /// Build recipe for the fetched source, a shell line (default:
192 /// `cargo build --release`). Gets `LIBTORCH_PATH`,
193 /// `FDL_GPU_FEATURE` and `LD_LIBRARY_PATH`.
194 #[option]
195 pub source_build: Option<String>,
196 /// Built artifact, relative to the project directory, e.g.
197 /// `target/release/train`.
198 #[option]
199 pub source_bin: Option<String>,
200 /// libtorch variant to acquire into `~/.flodl/libtorch/` before
201 /// building or running: `auto`, `cpu`, `cu126`, `cu128`, `rocm7.0`,
202 /// `rocm7.1`.
203 /// `auto` routes on this box's own devices, so one image serves
204 /// both vendors. Default: whatever is already active here.
205 #[option]
206 pub libtorch: Option<String>,
207 /// Logical host name in the roster (default: this machine's
208 /// hostname).
209 #[option]
210 pub host: Option<String>,
211 /// GPU device ids to offer, comma-separated (default: all GPUs
212 /// on this host).
213 #[option]
214 pub devices: Option<String>,
215 /// Keep re-dialing across runs with backoff instead of exiting
216 /// when the agent does.
217 #[option]
218 pub persist: bool,
219 /// Dataset source root on this box, shipped to this host's ranks
220 /// (with `--data-source`, the mountpoint; default `/flodl/data`).
221 #[option]
222 pub data_path: Option<String>,
223 /// Transport that establishes the source root when it is not
224 /// already mounted, e.g. `sshfs://user@ctrl:/flodl/data`.
225 #[option]
226 pub data_source: Option<String>,
227 /// Integrated-GPU host-RAM share of this box, a fraction of its
228 /// physical RAM, e.g. 0.5 (APU boxes only; discrete GPUs ignore
229 /// it). Ships to this host's ranks like `--data-path` does.
230 #[option]
231 pub gpu_ram_share: Option<f64>,
232 /// Skip the pre-dial model-signature probe (a short CPU-only
233 /// re-run of the training binary; admission uses the signature to
234 /// refuse a box building a different model). Skip it for a binary
235 /// whose startup is too heavy to run twice per dial.
236 #[option]
237 pub no_sig_probe: bool,
238}
239
240/// Provision a walk-in farm in one pass: the farm overlay
241/// (`fdl.<label>.yml`, token stamped), an ed25519 join key born in
242/// `./.fdl/<label>/` so it cannot be shared across farms by
243/// construction, the guardrailed `authorized_keys` line for the chosen
244/// door, the paste-ready worker yml, a publish recipe derived from the
245/// training crate's own manifest, and a build-freshness report. A farm
246/// IS an env overlay: `fdl @<label> <cmd>` targets it afterwards.
247/// Regenerating credentials for a new farm instantiation is
248/// `fdl join-config <label> --regen`.
249#[derive(crate::FdlArgs, Debug)]
250pub struct JoinConfigArgs {
251 /// Farm label — names `fdl.<label>.yml` and `.fdl/<label>/`.
252 /// Defaults to the active env (`fdl @<label> join-config`).
253 #[arg]
254 pub label: Option<String>,
255 /// How workers reach the join sshd: `[user@]host[:port]` (default:
256 /// this box's hostname, the invoking user, port 22).
257 #[option]
258 pub controller: Option<String>,
259 /// Guardrail door the join key opens: `b` (rrsync source pull, the
260 /// publish-then-join default), `a` (read-only sftp data mount), or
261 /// `nologin` (tunnel only).
262 #[option(choices = &["b", "a", "nologin"])]
263 pub door: Option<String>,
264 /// Training crate to derive the publish recipe from (default: the
265 /// current directory; no crate there is fine — a farm can be
266 /// config-only).
267 #[option]
268 pub crate_dir: Option<String>,
269 /// Dataset source root workers read; under door `a` it is also the
270 /// sshfs mountpoint (default /flodl/data there).
271 #[option]
272 pub data_path: Option<String>,
273 /// Integrated-GPU host-RAM share stamped into the worker yml
274 /// (APU fleets; discrete GPUs ignore it).
275 #[option]
276 pub gpu_ram_share: Option<f64>,
277 /// Regenerate credentials (key and token) without asking. Workers
278 /// holding the old ones stop being admitted.
279 #[option]
280 pub regen: bool,
281 /// Install the guardrailed line into this user's own
282 /// `~/.ssh/authorized_keys` without asking (the wizard's default
283 /// offer). Only the wizard's own line is ever touched; `/etc/ssh`
284 /// never is.
285 #[option]
286 pub install_key: bool,
287 /// Also emit a cloud-init user-data file: the worker yml, private
288 /// key and systemd unit embedded, so a cloud instance boots
289 /// straight into `fdl join`. A SECRET artifact (key + token
290 /// inside).
291 #[option]
292 pub cloud_init: bool,
293 /// The instance user the cloud-init variant provisions for
294 /// (default: ubuntu). Images that log in as root want `root`;
295 /// DigitalOcean and the AMD Developer Cloud are that shape.
296 #[option]
297 pub cloud_init_user: Option<String>,
298 /// Skip the authorized_keys install (print + notes only).
299 #[option]
300 pub no_install_key: bool,
301 /// Install into this authorized_keys file instead of the invoking
302 /// user's `~/.ssh/authorized_keys` — for a door the default cannot
303 /// reach, such as an sshd in a container (its key file is a bind
304 /// mount) or a host using `AuthorizedKeysFile
305 /// /etc/ssh/authorized_keys.d/%u`. Still consent-gated, still
306 /// touches only the wizard's own line; `/etc/ssh` is refused.
307 #[option]
308 pub authorized_keys: Option<String>,
309 /// Accept every default without prompting (non-interactive; reuses
310 /// existing credentials unless `--regen`). Never consents to the
311 /// authorized_keys install: that takes the prompt or `--install-key`.
312 #[option]
313 pub yes: bool,
314 /// Report as JSON on stdout. Secrets appear as file paths, never
315 /// payloads.
316 #[option]
317 pub json: bool,
318}
319
320/// Publish a training run for a fleet to pull.
321///
322/// The controller side of compiling on the node: resolves a source spec
323/// into a served directory, builds it once as a gate, and writes the run
324/// manifest workers read. Chaining runs on a standing fleet is then one
325/// command — publish again and every box picks the new run up on its next
326/// dial, with nothing to edit on any worker.
327///
328/// Arguments after a standalone `--` are the training binary's own, and
329/// they go in the manifest rather than into any worker's config: they must
330/// match the run, because rank children re-enter the binary with them, so
331/// a fleet carrying its own copy would train the next run with the
332/// previous one's hyperparameters.
333///
334/// The build proves the tree for THIS box's libtorch variant, and one
335/// build is all it is: every worker still compiles its own, since a
336/// controller producing binaries for N variants is a build matrix. A gate
337/// needs no GPU libtorch — `fdl libtorch download --cpu` is enough —
338/// because it is catching user-code errors, not shipping an artifact.
339///
340/// Exit code: 0 when the run is published; 1 otherwise, and a failed gate
341/// publishes nothing, so the fleet keeps running whatever it had.
342#[derive(crate::FdlArgs, Debug)]
343pub struct PublishArgs {
344 /// Source to publish: `file:///abs/path`,
345 /// `rsync://[user@]host[:port]:/abs/path`, or
346 /// `git+https://host/owner/repo#<tag|branch|sha>`.
347 #[arg]
348 pub source: Option<String>,
349 /// Built artifact, relative to the project directory — what workers
350 /// run. Required: a workspace member's build lands in the WORKSPACE
351 /// `target/`, so no rule fdl invented would be right for everyone.
352 #[option]
353 pub bin: Option<String>,
354 /// Project directory inside the tree (default: its root). Governs
355 /// the build and the run both.
356 #[option]
357 pub cwd: Option<String>,
358 /// Build recipe, a shell line (default: `cargo build --release`).
359 /// Gets `LIBTORCH_PATH`, `FDL_GPU_FEATURE` and `LD_LIBRARY_PATH`.
360 #[option]
361 pub build: Option<String>,
362 /// Served directory (default: `~/.flodl/run`). The tree lands in
363 /// `<dir>/tree`, which is what a worker's source spec points at.
364 #[option]
365 pub to: Option<String>,
366 /// Skip the build gate. Publishes source nothing has compiled, so
367 /// the first worker to fetch it is where a broken build surfaces.
368 #[option]
369 pub no_build: bool,
370 /// Identity file for a source pulled over ssh (`rsync -e ssh -i`).
371 #[option]
372 pub identity: Option<String>,
373 /// Emit the report as JSON on stdout (notes stay on stderr). The
374 /// machine twin of the human report, for dashboards and scripts.
375 #[option]
376 pub json: bool,
377 /// Extra check-build against another libtorch variant (a subpath
378 /// under `<project>/libtorch/`, e.g. `precompiled/rocm70`).
379 /// Repeatable. The primary gate proves only this box's variant, so
380 /// a break that exists only under the other vendor's feature would
381 /// land on a worker. Needs no GPU; a flodl-linking crate still
382 /// needs that vendor's dev headers here (a package install, and
383 /// the failure names the exact one).
384 #[option]
385 pub gate: Vec<String>,
386}
387
388impl PublishArgs {
389 /// Credentials for a source pulled over ssh. The spec itself carries
390 /// user, host and port, so only the key can be missing.
391 pub fn ssh_config(&self) -> Option<crate::config::SshConfig> {
392 self.identity.as_ref().map(|id| crate::config::SshConfig {
393 identity_file: Some(id.clone()),
394 ..Default::default()
395 })
396 }
397}
398
399/// Generate flodl API reference.
400#[derive(crate::FdlArgs, Debug)]
401pub struct ApiRefArgs {
402 /// Emit machine-readable JSON.
403 #[option]
404 pub json: bool,
405 /// Explicit flodl source path (defaults to detected project root).
406 #[option]
407 pub path: Option<String>,
408}
409
410/// Scaffold a new floDl project.
411///
412/// Three modes, mutually exclusive:
413/// default (no flag) — Docker with host-mounted libtorch (recommended)
414/// --docker — Docker with libtorch baked into the image
415/// --native — no Docker, host-provided libtorch + cargo
416#[derive(crate::FdlArgs, Debug)]
417pub struct InitArgs {
418 /// New project directory name.
419 #[arg]
420 pub name: Option<String>,
421 /// Generate a Docker scaffold with libtorch baked into the image.
422 #[option]
423 pub docker: bool,
424 /// Generate a native scaffold (no Docker; libtorch provided on the host).
425 #[option]
426 pub native: bool,
427 /// Also scaffold the flodl-hf HuggingFace playground (skips the prompt).
428 #[option]
429 pub with_hf: bool,
430}
431
432/// Add a flodl ecosystem crate to the current flodl project.
433///
434/// Currently supports `flodl-hf` (alias: `hf`). Two modes (combinable):
435///
436/// - `--playground`: drops a standalone cargo crate under `./flodl-hf/`
437/// with pinned deps and a one-file AutoModel example, plus a
438/// `flodl-hf:` entry in the root `fdl.yml` so `fdl flodl-hf <cmd>`
439/// routes into it. Try-it-out path; doesn't touch `Cargo.toml`.
440/// - `--install`: appends `flodl-hf = "=X.Y.Z"` (default features) to
441/// the root `Cargo.toml` `[dependencies]`. Wires the crate into the
442/// user's own code; doesn't create a subdir.
443///
444/// With neither flag, an interactive prompt asks. Non-tty stdin errors
445/// loudly rather than silently picking a default.
446#[derive(crate::FdlArgs, Debug)]
447pub struct AddArgs {
448 /// Target to scaffold (currently: `flodl-hf` or the alias `hf`).
449 #[arg]
450 pub target: Option<String>,
451 /// Drop a sandbox playground under `./flodl-hf/`.
452 #[option]
453 pub playground: bool,
454 /// Add as a dependency in the root `Cargo.toml`.
455 #[option]
456 pub install: bool,
457}
458
459/// Install or update fdl globally (~/.local/bin/fdl).
460#[derive(crate::FdlArgs, Debug)]
461pub struct InstallArgs {
462 /// Check for updates without installing.
463 #[option]
464 pub check: bool,
465 /// Symlink to the current binary (tracks local builds).
466 #[option]
467 pub dev: bool,
468}
469
470/// List installed libtorch variants.
471#[derive(crate::FdlArgs, Debug)]
472pub struct LibtorchListArgs {
473 /// Emit machine-readable JSON.
474 #[option]
475 pub json: bool,
476}
477
478/// Activate a libtorch variant.
479#[derive(crate::FdlArgs, Debug)]
480pub struct LibtorchActivateArgs {
481 /// Variant to activate (as shown by `fdl libtorch list`).
482 #[arg]
483 pub variant: Option<String>,
484}
485
486/// Remove a libtorch variant.
487#[derive(crate::FdlArgs, Debug)]
488pub struct LibtorchRemoveArgs {
489 /// Variant to remove (as shown by `fdl libtorch list`).
490 #[arg]
491 pub variant: Option<String>,
492}
493
494/// Download a pre-built libtorch variant.
495#[derive(crate::FdlArgs, Debug)]
496pub struct LibtorchDownloadArgs {
497 /// Force the CPU variant.
498 #[option]
499 pub cpu: bool,
500 /// Pick a specific CUDA version (instead of auto-detect).
501 #[option(choices = &["12.6", "12.8"])]
502 pub cuda: Option<String>,
503 /// Pick an AMD ROCm build instead of CUDA. Both cover the same gfx
504 /// targets; pick the one matching this host's own ROCm, since the
505 /// host runtime loads ahead of the bundled one.
506 #[option(choices = &["7.0", "7.1"])]
507 pub rocm: Option<String>,
508 /// Install libtorch to this directory (default: project libtorch/).
509 #[option]
510 pub path: Option<String>,
511 /// Do not activate after download.
512 #[option]
513 pub no_activate: bool,
514 /// Show what would happen without downloading.
515 #[option]
516 pub dry_run: bool,
517}
518
519/// Build libtorch from source.
520#[derive(crate::FdlArgs, Debug)]
521pub struct LibtorchBuildArgs {
522 /// Override CUDA architectures (semicolon-separated, e.g. "6.1;12.0").
523 #[option]
524 pub archs: Option<String>,
525 /// Parallel compilation jobs.
526 #[option(default = "6")]
527 pub jobs: usize,
528 /// Force Docker build (isolated, reproducible).
529 #[option]
530 pub docker: bool,
531 /// Force native build (faster, requires host toolchain).
532 #[option]
533 pub native: bool,
534 /// Show what would happen without building.
535 #[option]
536 pub dry_run: bool,
537}
538
539/// Build NCCL from NVIDIA source for a heterogeneous-arch rig.
540#[derive(crate::FdlArgs, Debug)]
541pub struct NcclBuildArgs {
542 /// NCCL git tag to build (e.g. "v2.27.5-1"). Default: infer from the
543 /// active libtorch's bundled NCCL version string (the version we must
544 /// match for cross-rank handshake).
545 #[option]
546 pub tag: Option<String>,
547 /// Override CUDA architectures (semicolon-separated, e.g. "6.1;12.0").
548 /// Default: auto-detect from local GPUs.
549 #[option]
550 pub archs: Option<String>,
551 /// Parallel compilation jobs.
552 #[option(default = "6")]
553 pub jobs: usize,
554 /// Show what would happen without building.
555 #[option]
556 pub dry_run: bool,
557}
558
559/// Install AI coding assistant skills.
560#[derive(crate::FdlArgs, Debug)]
561pub struct SkillInstallArgs {
562 /// Target tool (defaults to auto-detect).
563 #[option]
564 pub tool: Option<String>,
565 /// Specific skill name (defaults to all detected skills).
566 #[option]
567 pub skill: Option<String>,
568}
569
570/// List cached `--fdl-schema` outputs.
571#[derive(crate::FdlArgs, Debug)]
572pub struct SchemaListArgs {
573 /// Emit machine-readable JSON.
574 #[option]
575 pub json: bool,
576}
577
578/// Clear cached schemas. No command name clears all.
579#[derive(crate::FdlArgs, Debug)]
580pub struct SchemaClearArgs {
581 /// Command name to clear (defaults to all).
582 #[arg]
583 pub cmd: Option<String>,
584}
585
586/// Re-probe each entry and rewrite the cache.
587#[derive(crate::FdlArgs, Debug)]
588pub struct SchemaRefreshArgs {
589 /// Command name to refresh (defaults to all).
590 #[arg]
591 pub cmd: Option<String>,
592}
593
594// ---------------------------------------------------------------------------
595// Registry
596// ---------------------------------------------------------------------------
597
598/// One built-in command (or sub-command) slot.
599pub struct BuiltinSpec {
600 /// Path from the top-level command name. `["install"]`,
601 /// `["libtorch", "download"]`.
602 pub path: &'static [&'static str],
603 /// One-line description for `fdl -h` listing. `None` = hidden
604 /// (reserved for collision detection but not shown in help).
605 pub description: Option<&'static str>,
606 /// Constructor for the command's schema. `None` for parent commands
607 /// that only group sub-commands (e.g. `libtorch` itself has no args)
608 /// or for leaves whose argv is parsed by hand (`config show`,
609 /// `completions`, `autocomplete`).
610 pub schema_fn: Option<fn() -> Schema>,
611}
612
613/// Ordered registry of every built-in. Order drives `fdl -h` and the
614/// top-level completion word list, so it mirrors today's `BUILTINS`
615/// const in `main.rs`.
616pub fn registry() -> &'static [BuiltinSpec] {
617 static REG: &[BuiltinSpec] = &[
618 BuiltinSpec {
619 path: &["setup"],
620 description: Some("Interactive guided setup"),
621 schema_fn: Some(SetupArgs::schema),
622 },
623 BuiltinSpec {
624 path: &["libtorch"],
625 description: Some("Manage libtorch installations"),
626 schema_fn: None,
627 },
628 BuiltinSpec {
629 path: &["libtorch", "download"],
630 description: Some("Download pre-built libtorch"),
631 schema_fn: Some(LibtorchDownloadArgs::schema),
632 },
633 BuiltinSpec {
634 path: &["libtorch", "build"],
635 description: Some("Build libtorch from source"),
636 schema_fn: Some(LibtorchBuildArgs::schema),
637 },
638 BuiltinSpec {
639 path: &["libtorch", "list"],
640 description: Some("Show installed variants"),
641 schema_fn: Some(LibtorchListArgs::schema),
642 },
643 BuiltinSpec {
644 path: &["libtorch", "activate"],
645 description: Some("Set active variant"),
646 schema_fn: Some(LibtorchActivateArgs::schema),
647 },
648 BuiltinSpec {
649 path: &["libtorch", "remove"],
650 description: Some("Remove a variant"),
651 schema_fn: Some(LibtorchRemoveArgs::schema),
652 },
653 BuiltinSpec {
654 path: &["libtorch", "info"],
655 description: Some("Show active variant details"),
656 schema_fn: None,
657 },
658 BuiltinSpec {
659 path: &["nccl"],
660 description: Some("Build NCCL from source (heterogeneous-arch bridge)"),
661 schema_fn: None,
662 },
663 BuiltinSpec {
664 path: &["nccl", "build"],
665 description: Some("Compile libnccl for the local GPU arch"),
666 schema_fn: Some(NcclBuildArgs::schema),
667 },
668 BuiltinSpec {
669 path: &["init"],
670 description: Some("Scaffold a new floDl project"),
671 schema_fn: Some(InitArgs::schema),
672 },
673 BuiltinSpec {
674 path: &["add"],
675 description: Some("Add a flodl ecosystem crate (currently: flodl-hf)"),
676 schema_fn: Some(AddArgs::schema),
677 },
678 BuiltinSpec {
679 path: &["diagnose"],
680 description: Some("System and GPU diagnostics"),
681 schema_fn: Some(DiagnoseArgs::schema),
682 },
683 BuiltinSpec {
684 path: &["probe"],
685 description: Some("Cluster readiness probe (GPU + libtorch + data mount)"),
686 schema_fn: Some(ProbeArgs::schema),
687 },
688 BuiltinSpec {
689 path: &["status"],
690 description: Some("Live cluster run status (membership, lifecycle phase)"),
691 schema_fn: Some(StatusArgs::schema),
692 },
693 BuiltinSpec {
694 path: &["start"],
695 description: Some("Fire the start switch of a staging cluster run"),
696 schema_fn: Some(StartArgs::schema),
697 },
698 BuiltinSpec {
699 path: &["publish"],
700 description: Some("Publish a training run for a fleet to pull"),
701 schema_fn: Some(PublishArgs::schema),
702 },
703 BuiltinSpec {
704 path: &["join"],
705 description: Some("Join a cluster run's window as a self-deployed worker"),
706 schema_fn: Some(JoinArgs::schema),
707 },
708 BuiltinSpec {
709 path: &["join-config"],
710 description: Some("Provision a walk-in farm: overlay, credentials, worker yml"),
711 schema_fn: Some(JoinConfigArgs::schema),
712 },
713 BuiltinSpec {
714 path: &["install"],
715 description: Some("Install or update fdl globally"),
716 schema_fn: Some(InstallArgs::schema),
717 },
718 BuiltinSpec {
719 path: &["skill"],
720 description: Some("Manage AI coding assistant skills"),
721 schema_fn: None,
722 },
723 BuiltinSpec {
724 path: &["skill", "install"],
725 description: Some("Install skills for the detected tool"),
726 schema_fn: Some(SkillInstallArgs::schema),
727 },
728 BuiltinSpec {
729 path: &["skill", "list"],
730 description: Some("Show available skills"),
731 schema_fn: None,
732 },
733 BuiltinSpec {
734 path: &["api-ref"],
735 description: Some("Generate flodl API reference"),
736 schema_fn: Some(ApiRefArgs::schema),
737 },
738 BuiltinSpec {
739 path: &["config"],
740 description: Some("Inspect resolved project configuration"),
741 schema_fn: None,
742 },
743 BuiltinSpec {
744 path: &["config", "show"],
745 description: Some("Print the resolved merged config"),
746 schema_fn: None,
747 },
748 BuiltinSpec {
749 path: &["schema"],
750 description: Some("Inspect, clear, or refresh cached --fdl-schema outputs"),
751 schema_fn: None,
752 },
753 BuiltinSpec {
754 path: &["schema", "list"],
755 description: Some("Show every cached schema with status"),
756 schema_fn: Some(SchemaListArgs::schema),
757 },
758 BuiltinSpec {
759 path: &["schema", "clear"],
760 description: Some("Delete cached schema(s)"),
761 schema_fn: Some(SchemaClearArgs::schema),
762 },
763 BuiltinSpec {
764 path: &["schema", "refresh"],
765 description: Some("Re-probe each entry and rewrite the cache"),
766 schema_fn: Some(SchemaRefreshArgs::schema),
767 },
768 BuiltinSpec {
769 path: &["completions"],
770 description: Some("Emit shell completion script (bash|zsh|fish)"),
771 schema_fn: None,
772 },
773 BuiltinSpec {
774 path: &["autocomplete"],
775 description: Some("Install completions into the detected shell"),
776 schema_fn: None,
777 },
778 // Hidden: `version` is covered by `-V` / `--version` but still
779 // reserved as a top-level built-in name.
780 BuiltinSpec {
781 path: &["version"],
782 description: None,
783 schema_fn: None,
784 },
785 ];
786 REG
787}
788
789/// True when `name` is a reserved top-level built-in (visible or hidden).
790pub fn is_builtin_name(name: &str) -> bool {
791 registry()
792 .iter()
793 .any(|s| s.path.len() == 1 && s.path[0] == name)
794}
795
796/// Visible top-level built-ins as `(name, description)` pairs, in
797/// registry order. Feeds `run::print_project_help` and the fallback
798/// `print_usage`.
799pub fn visible_top_level() -> Vec<(&'static str, &'static str)> {
800 registry()
801 .iter()
802 .filter(|s| s.path.len() == 1)
803 .filter_map(|s| s.description.map(|d| (s.path[0], d)))
804 .collect()
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use std::collections::HashSet;
811
812 #[test]
813 fn registry_has_no_duplicate_paths() {
814 let mut seen = HashSet::new();
815 for s in registry() {
816 let key = s.path.join(" ");
817 assert!(seen.insert(key.clone()), "duplicate registry path: {key}");
818 }
819 }
820
821 #[test]
822 fn hidden_entries_have_no_description() {
823 for s in registry() {
824 if s.path == ["version"] {
825 assert!(
826 s.description.is_none(),
827 "`version` is hidden but carries a description"
828 );
829 }
830 }
831 }
832
833 #[test]
834 fn every_parent_has_at_least_one_child() {
835 let parents: HashSet<&str> = registry()
836 .iter()
837 .filter(|s| s.path.len() == 1 && s.schema_fn.is_none() && s.description.is_some())
838 .map(|s| s.path[0])
839 .collect();
840
841 // `completions`, `autocomplete` are leaves with no schema — exclude
842 // them by checking that parents have at least one 2-path child.
843 for parent in &parents {
844 let has_child = registry()
845 .iter()
846 .any(|s| s.path.len() == 2 && s.path[0] == *parent);
847 if !has_child {
848 // `completions` / `autocomplete` / `version` end up here by
849 // virtue of having no children; they are leaf built-ins.
850 continue;
851 }
852 assert!(has_child, "parent `{parent}` has no child entries");
853 }
854 }
855
856 #[test]
857 fn top_level_dispatched_by_main_is_in_registry() {
858 // Compile-time guard: every match arm target in main.rs is listed
859 // here. Keeping the list local (rather than introspecting main.rs)
860 // documents the coupling explicitly.
861 let dispatched = [
862 "setup",
863 "libtorch",
864 "nccl",
865 "diagnose",
866 "probe",
867 "status",
868 "start",
869 "publish",
870 "join",
871 "join-config",
872 "api-ref",
873 "init",
874 "add",
875 "install",
876 "skill",
877 "schema",
878 "completions",
879 "autocomplete",
880 "config",
881 "version",
882 ];
883 for name in &dispatched {
884 assert!(
885 is_builtin_name(name),
886 "`{name}` dispatched by main.rs but missing from registry"
887 );
888 }
889 }
890
891 #[test]
892 fn visible_top_level_matches_help_ordering() {
893 let top = visible_top_level();
894 let names: Vec<&str> = top.iter().map(|(n, _)| *n).collect();
895 // Lock in the order that `fdl -h` depends on.
896 assert_eq!(
897 names,
898 vec![
899 "setup",
900 "libtorch",
901 "nccl",
902 "init",
903 "add",
904 "diagnose",
905 "probe",
906 "status",
907 "start",
908 "publish",
909 "join",
910 "join-config",
911 "install",
912 "skill",
913 "api-ref",
914 "config",
915 "schema",
916 "completions",
917 "autocomplete",
918 ]
919 );
920 }
921
922 #[test]
923 fn libtorch_download_schema_carries_cuda_choices() {
924 let spec = registry()
925 .iter()
926 .find(|s| s.path == ["libtorch", "download"])
927 .expect("libtorch download entry present");
928 let schema = (spec.schema_fn.expect("download has schema"))();
929 let cuda = schema
930 .options
931 .get("cuda")
932 .expect("`--cuda` option declared");
933 let choices = cuda.choices.as_ref().expect("--cuda has choices");
934 let values: Vec<String> = choices
935 .iter()
936 .filter_map(|v| v.as_str().map(str::to_string))
937 .collect();
938 assert_eq!(values, vec!["12.6".to_string(), "12.8".into()]);
939 }
940}