use flodl_cli::{
add, api_ref, builtins, cli_error, cluster, completions, config, diagnose, gpus, init,
overlay, parse_or_schema_from, probe, run, setup, skill, status, style, update_check,
};
use builtins::{
AddArgs, ApiRefArgs, DiagnoseArgs, InitArgs, InstallArgs, ProbeArgs, SetupArgs,
SkillInstallArgs, StatusArgs,
};
use std::env;
use std::process::ExitCode;
fn main() -> ExitCode {
let _update_check_guard = update_check::Guard::new();
let raw_args: Vec<String> = env::args().collect();
let args = match extract_ansi_flags(&raw_args) {
Ok((args, choice)) => {
if let Some(c) = choice {
style::set_color_choice(c);
unsafe {
env::set_var(
"FLODL_COLOR",
match c {
style::ColorChoice::Always => "always",
style::ColorChoice::Never => "never",
style::ColorChoice::Auto => "auto",
},
);
}
} else if let Ok(v) = env::var("FLODL_COLOR") {
match v.as_str() {
"always" => style::set_color_choice(style::ColorChoice::Always),
"never" => style::set_color_choice(style::ColorChoice::Never),
_ => {}
}
}
args
}
Err(msg) => {
cli_error!("{msg}");
return ExitCode::FAILURE;
}
};
let (args, verbosity) = extract_verbosity(&args);
if let Some(v) = verbosity {
unsafe {
env::set_var("FLODL_VERBOSITY", v.to_string());
}
}
if env::var_os("HOSTNAME").is_none() {
let host = crate::cluster::resolve_local_hostname();
if !host.is_empty() {
unsafe {
env::set_var("HOSTNAME", host);
}
}
}
let (args, no_append) = extract_no_append(&args);
let (args, no_prebuild) = extract_no_prebuild(&args);
let (args, gpus_spec) = match extract_gpus_flag(&args) {
Ok(pair) => pair,
Err(msg) => {
cli_error!("{msg}");
return ExitCode::FAILURE;
}
};
let fdl_env_var = env::var("FDL_ENV").ok();
let (active_env, args) = match resolve_env(&args, fdl_env_var.as_deref()) {
Ok(pair) => pair,
Err(msg) => {
cli_error!("{msg}");
return ExitCode::FAILURE;
}
};
if let Some(env_name) = active_env.as_deref() {
unsafe {
env::set_var(cluster::ENV_FDL_ENV, env_name);
}
}
let cmd = args.get(1).map(String::as_str).unwrap_or("--help");
match cmd {
"setup" => {
let cli: SetupArgs = parse_sub("fdl setup", &args[1..]);
let opts = setup::SetupOpts {
non_interactive: cli.non_interactive,
force: cli.force,
};
match setup::run(opts) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
cli_error!("{e}");
ExitCode::FAILURE
}
}
}
"libtorch" => dispatch_libtorch(&args),
"nccl" => dispatch_nccl(&args),
"diagnose" => {
let cli: DiagnoseArgs = parse_sub("fdl diagnose", &args[1..]);
diagnose::run(cli.json);
ExitCode::SUCCESS
}
"probe" => {
let cli: ProbeArgs = parse_sub("fdl probe", &args[1..]);
let code = probe::run(
cli.json,
cli.skip_mount,
cli.data_path,
cli.libtorch_path,
cli.docker,
);
if code == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE }
}
"status" => {
let cli: StatusArgs = parse_sub("fdl status", &args[1..]);
let code = status::run(cli.json, cli.addr.as_deref());
if code == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE }
}
"api-ref" => {
let cli: ApiRefArgs = parse_sub("fdl api-ref", &args[1..]);
match api_ref::run(cli.json, cli.path.as_deref()) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
cli_error!("{e}");
ExitCode::FAILURE
}
}
}
"init" => {
let cli: InitArgs = parse_sub("fdl init", &args[1..]);
match init::run(cli.name.as_deref(), cli.docker, cli.native, cli.with_hf) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
cli_error!("{e}");
ExitCode::FAILURE
}
}
}
"add" => {
let cli: AddArgs = parse_sub("fdl add", &args[1..]);
match add::run(cli.target.as_deref(), cli.playground, cli.install) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
cli_error!("{e}");
ExitCode::FAILURE
}
}
}
"install" => {
let cli: InstallArgs = parse_sub("fdl install", &args[1..]);
cmd_install(cli.check, cli.dev)
}
"skill" => dispatch_skill(&args),
"schema" => dispatch_schema(&args),
"completions" => {
let shell = args.get(2).map(String::as_str).unwrap_or("bash");
let cwd = env::current_dir().unwrap_or_default();
let project = load_project_config(&cwd, active_env.as_deref());
completions::generate(
shell,
project.as_ref().map(|(p, r)| (p, r.as_path())),
active_env.as_deref(),
);
ExitCode::SUCCESS
}
"autocomplete" => {
let cwd = env::current_dir().unwrap_or_default();
let project = load_project_config(&cwd, active_env.as_deref());
completions::autocomplete(project.as_ref().map(|(p, r)| (p, r.as_path())));
ExitCode::SUCCESS
}
"config" => cmd_config_show(&args[1..], active_env.as_deref()),
"--help" | "-h" => {
let cwd = env::current_dir().unwrap_or_default();
let help_env = help_env_or_note(&cwd, active_env.as_deref());
if let Some((project, root)) = load_project_config(&cwd, help_env) {
run::print_project_help(&project, &root, help_env);
} else {
print_usage();
}
ExitCode::SUCCESS
}
"version" | "--version" | "-V" => {
println!("flodl-cli {}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS
}
other => dispatch_config(
other,
&args,
active_env.as_deref(),
no_append,
no_prebuild,
gpus_spec.as_ref(),
),
}
}
fn resolve_env(
args: &[String],
fdl_env: Option<&str>,
) -> Result<(Option<String>, Vec<String>), String> {
let (args, flag_env) = extract_env_flag(args)?;
let (args, at_env) = extract_at_env(&args)?;
let cli_env = match (flag_env, at_env) {
(Some(f), Some(a)) if f != a => {
return Err(format!(
"conflicting env selectors: `--env {f}` and `@{a}` — pick one"
));
}
(Some(v), _) | (None, Some(v)) => Some(v),
(None, None) => None,
};
if let Some(env_name) = cli_env {
return Ok((Some(env_name), args));
}
if let Some(env_name) = fdl_env {
if !env_name.is_empty() {
return Ok((Some(env_name.to_string()), args));
}
}
Ok((None, args))
}
fn extract_gpus_flag(
args: &[String],
) -> Result<(Vec<String>, Option<gpus::GpusSpec>), String> {
let mut out = Vec::with_capacity(args.len());
let mut spec: Option<gpus::GpusSpec> = None;
let mut i = 0;
while i < args.len() {
let a = &args[i];
if a == "--" {
out.extend(args[i..].iter().cloned());
break;
}
if a == "--gpus" {
let value = args.get(i + 1).ok_or_else(|| {
"--gpus requires a value (e.g. `--gpus 0,1` or `--gpus all`)"
.to_string()
})?;
if value.is_empty() || value.starts_with('-') {
return Err(format!("--gpus requires a value, got `{value}`"));
}
if spec.is_some() {
return Err("--gpus specified more than once".to_string());
}
spec = Some(gpus::GpusSpec::parse(value)?);
i += 2;
continue;
}
if let Some(value) = a.strip_prefix("--gpus=") {
if spec.is_some() {
return Err("--gpus specified more than once".to_string());
}
if value.is_empty() {
return Err(
"--gpus= requires a value (e.g. `--gpus=0,1`)".to_string(),
);
}
spec = Some(gpus::GpusSpec::parse(value)?);
i += 1;
continue;
}
out.push(a.clone());
i += 1;
}
Ok((out, spec))
}
fn extract_env_flag(args: &[String]) -> Result<(Vec<String>, Option<String>), String> {
let mut out = Vec::with_capacity(args.len());
let mut env: Option<String> = None;
let mut i = 0;
while i < args.len() {
let a = &args[i];
if a == "--" {
out.extend(args[i..].iter().cloned());
break;
}
if a == "--env" {
let value = args.get(i + 1).ok_or_else(|| {
"--env requires a value (e.g. `--env ci`)".to_string()
})?;
if value.is_empty() || value.starts_with('-') {
return Err(format!("--env requires a value, got `{value}`"));
}
if env.is_some() {
return Err("--env specified more than once".to_string());
}
env = Some(value.clone());
i += 2;
continue;
}
if let Some(value) = a.strip_prefix("--env=") {
if env.is_some() {
return Err("--env specified more than once".to_string());
}
if value.is_empty() {
return Err("--env= requires a value (e.g. `--env=ci`)".to_string());
}
env = Some(value.to_string());
i += 1;
continue;
}
out.push(a.clone());
i += 1;
}
Ok((out, env))
}
fn extract_at_env(args: &[String]) -> Result<(Vec<String>, Option<String>), String> {
let mut out = Vec::with_capacity(args.len());
let mut env: Option<String> = None;
let mut command_seen = false;
for (i, arg) in args.iter().enumerate() {
if i == 0 || command_seen {
out.push(arg.clone());
continue;
}
if arg == "--" {
command_seen = true;
out.push(arg.clone());
continue;
}
if let Some(name) = arg.strip_prefix('@') {
if name.is_empty() {
return Err(
"`@` requires an env name (e.g. `@cluster`)".to_string(),
);
}
if env.is_some() {
return Err(
"env selector (`@<env>`) specified more than once".to_string(),
);
}
env = Some(name.to_string());
continue;
}
if !arg.starts_with('-') {
command_seen = true;
}
out.push(arg.clone());
}
Ok((out, env))
}
fn help_env_or_note<'a>(cwd: &std::path::Path, env: Option<&'a str>) -> Option<&'a str> {
let name = env?;
match config::find_config(cwd) {
Some(base) if overlay::find_env_file(&base, name).is_some() => Some(name),
Some(_) => {
eprintln!("fdl: env `{name}` not found; showing base help");
None
}
None => None,
}
}
pub(crate) fn parse_sub<T: flodl_cli::FdlArgsTrait>(program: &str, tail: &[String]) -> T {
let mut argv = Vec::with_capacity(tail.len() + 1);
argv.push(program.to_string());
argv.extend(tail.iter().skip(1).cloned());
parse_or_schema_from::<T>(&argv)
}
fn dispatch_skill(args: &[String]) -> ExitCode {
let sub = args.get(2).map(String::as_str).unwrap_or("--help");
match sub {
"install" => {
let cli: SkillInstallArgs = parse_sub("fdl skill install", &args[2..]);
match skill::install(cli.tool.as_deref(), cli.skill.as_deref()) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
cli_error!("{e}");
ExitCode::FAILURE
}
}
}
"list" => {
skill::list();
ExitCode::SUCCESS
}
"--help" | "-h" => {
skill::print_usage();
ExitCode::SUCCESS
}
other => {
eprintln!("unknown skill command: {other}");
skill::print_usage();
ExitCode::FAILURE
}
}
}
mod cli;
use cli::libtorch::dispatch_libtorch;
use cli::nccl::dispatch_nccl;
use cli::schema::dispatch_schema;
use cli::install::cmd_install;
use cli::config::{cmd_config_show, dispatch_config, load_project_config};
pub(crate) fn print_usage() {
println!("flodl-cli {}", env!("CARGO_PKG_VERSION"));
println!();
println!("The floDl companion tool: setup, libtorch, diagnostics, API reference.");
println!("Works anywhere. Uses project root when available, ~/.flodl/ otherwise.");
println!();
println!("USAGE:");
println!(" fdl [options] <command> [command-options]");
println!();
println!("GLOBAL OPTIONS:");
println!(" --env <name> Use fdl.<name>.yml overlay (also: FDL_ENV=<name>)");
println!(" --ansi Force ANSI color output");
println!(" --no-ansi Disable ANSI color output");
println!(" -v Verbose output (DDP sync, data loading detail)");
println!(" -vv Debug output (per-batch timing, loop internals)");
println!(" -vvv Trace output (maximum detail)");
println!(" -q, --quiet Suppress all non-error output");
println!(" --no-append Drop a run command's `append:` suffix (cargo / runner defaults)");
println!(" --no-prebuild Skip the cluster pre-flight build (assumes binaries are fresh)");
println!();
println!("COMMANDS:");
println!(" setup Interactive guided setup");
println!(" libtorch Manage libtorch installations");
println!(" init <name> Scaffold a new floDl project");
println!(" --docker Generate Docker-based scaffold (libtorch baked in)");
println!(" add <target> Add a flodl ecosystem crate (currently: flodl-hf)");
println!(" diagnose System and GPU diagnostics");
println!(" --json Output as JSON");
println!(" install Install or update fdl globally (~/.local/bin)");
println!(" --check Check for updates without installing");
println!(" --dev Symlink to current binary (tracks local builds)");
println!(" skill Manage AI coding assistant skills");
println!(" install Install skills for detected tool (Claude, Cursor, ...)");
println!(" list Show available skills");
println!(" api-ref Generate flodl API reference");
println!(" --json Output as JSON");
println!(" --path <dir> Explicit flodl source path");
println!(" version Show version");
println!();
println!("Run `fdl --help` or `fdl <command> --help` for details.");
println!();
println!("INSTALL:");
println!(" cargo install flodl-cli # from crates.io");
println!(" fdl install # make current binary global (~/.local/bin/fdl)");
println!();
println!("EXAMPLES:");
println!(" fdl setup # first-time setup");
println!(" fdl libtorch download # download pre-built libtorch");
println!(" fdl libtorch list # show installed variants");
println!(" fdl init my-model # scaffold with mounted libtorch");
println!(" fdl diagnose # hardware + compatibility report");
println!(" fdl diagnose --json # machine-readable output");
println!(" fdl api-ref # generate API reference");
println!(" fdl api-ref --json # structured JSON for tooling");
}
fn extract_verbosity(args: &[String]) -> (Vec<String>, Option<u8>) {
let mut level: Option<u8> = None;
let mut filtered = Vec::with_capacity(args.len());
let mut past_dashdash = false;
for arg in args {
if past_dashdash {
filtered.push(arg.clone());
continue;
}
if arg == "--" {
past_dashdash = true;
filtered.push(arg.clone());
continue;
}
match arg.as_str() {
"-vvv" => level = Some(4), "-vv" => level = Some(3), "-v" => level = Some(2), "--quiet" | "-q" => level = Some(0), _ => filtered.push(arg.clone()),
}
}
(filtered, level)
}
fn extract_no_append(args: &[String]) -> (Vec<String>, bool) {
let mut found = false;
let mut filtered = Vec::with_capacity(args.len());
let mut past_dashdash = false;
for arg in args {
if past_dashdash {
filtered.push(arg.clone());
continue;
}
if arg == "--" {
past_dashdash = true;
filtered.push(arg.clone());
continue;
}
if arg == "--no-append" {
found = true;
continue;
}
filtered.push(arg.clone());
}
(filtered, found)
}
fn extract_no_prebuild(args: &[String]) -> (Vec<String>, bool) {
let mut found = false;
let mut filtered = Vec::with_capacity(args.len());
let mut past_dashdash = false;
for arg in args {
if past_dashdash {
filtered.push(arg.clone());
continue;
}
if arg == "--" {
past_dashdash = true;
filtered.push(arg.clone());
continue;
}
if arg == "--no-prebuild" {
found = true;
continue;
}
filtered.push(arg.clone());
}
(filtered, found)
}
fn extract_ansi_flags(
args: &[String],
) -> Result<(Vec<String>, Option<style::ColorChoice>), String> {
let mut ansi = false;
let mut no_ansi = false;
let mut filtered = Vec::with_capacity(args.len());
let mut past_dashdash = false;
for arg in args {
if past_dashdash {
filtered.push(arg.clone());
continue;
}
if arg == "--" {
past_dashdash = true;
filtered.push(arg.clone());
continue;
}
match arg.as_str() {
"--ansi" => ansi = true,
"--no-ansi" => no_ansi = true,
_ => filtered.push(arg.clone()),
}
}
let choice = match (ansi, no_ansi) {
(true, true) => return Err(
"--ansi and --no-ansi are mutually exclusive".to_string()
),
(true, false) => Some(style::ColorChoice::Always),
(false, true) => Some(style::ColorChoice::Never),
(false, false) => None,
};
Ok((filtered, choice))
}
#[cfg(test)]
#[path = "cli_tests.rs"]
mod tests;