#![forbid(unsafe_code)]
pub mod banner;
pub mod commands;
use crate::commands::ai_status::{ai_setup_command, ai_status_command, ai_test_command};
use crate::commands::batch::batch_command;
use crate::commands::clean::clean_command;
use crate::commands::cluster::cluster_command;
use crate::commands::completions::handle_completions;
use crate::commands::infra::{env_command, handle_license};
use crate::commands::output::{handle_diff, handle_export, handle_graph, handle_report};
use crate::commands::project::{init_command, template_command};
use crate::commands::provenance::provenance_verify_command;
use crate::commands::publish::publish_command;
use crate::commands::quality::{
deep_check_command, format_command, lint_command, touch_command, validate_command,
};
use crate::commands::run::{
debug_command, dry_run_command, handle_status, resume_command, run_command,
};
use anyhow::Result;
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "oxo-flow",
version,
about = "A Rust-native bioinformatics pipeline engine",
long_about = "oxo-flow is a high-performance, modular bioinformatics pipeline engine\n\
built from first principles in Rust. It supports conda, pixi, docker,\n\
singularity, and venv environments with DAG-based execution."
)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(global = true, short = 'v', long)]
verbose: bool,
#[arg(global = true, long)]
quiet: bool,
#[arg(global = true, long)]
no_color: bool,
#[arg(global = true, long)]
json: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Run {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: Option<PathBuf>,
#[arg(
short = 'j',
long,
default_value = "1",
help = "Maximum number of concurrent jobs"
)]
jobs: usize,
#[arg(short = 'k', long, help = "Continue execution when a job fails")]
keep_going: bool,
#[arg(short = 'd', long, help = "Working directory for execution")]
workdir: Option<PathBuf>,
#[arg(
short = 't',
long,
help = "Run only specific target rules (repeatable, prefix matching)"
)]
target: Vec<String>,
#[arg(
short = 'r',
long,
default_value = "0",
help = "Number of times to retry failed jobs"
)]
retry: u32,
#[arg(
long,
default_value = "0",
help = "Timeout per job in seconds (0 = disabled), or a duration like 1h/30m"
)]
timeout: String,
#[arg(long, help = "Resume only failed rules from a previous run")]
resume_failed: bool,
#[arg(
long,
help = "Execution profile name (loaded from profiles/<NAME>.toml or profiles/<NAME>.oxoflow next to the workflow; fills in config keys the workflow does not set)"
)]
profile: Option<String>,
#[arg(
long,
default_value = "0",
help = "Maximum CPU threads available for execution (0 = auto-detect)"
)]
max_threads: u32,
#[arg(
long,
default_value = "0",
help = "Maximum memory in MB available for execution (0 = auto-detect)"
)]
max_memory: u64,
#[arg(long, help = "Skip environment setup (assume environments are ready)")]
skip_env_setup: bool,
#[arg(long, help = "Skip automatic reference/index building")]
skip_ref_build: bool,
#[arg(long, help = "Directory for caching environment setup state")]
cache_dir: Option<PathBuf>,
#[arg(long, help = "Track output file checksums for later verification")]
provenance: bool,
#[arg(long, help = "Execute from a published .tar.zst bundle")]
bundle: Option<PathBuf>,
#[arg(
long = "yes",
help = "Skip the confirmation prompt when running from a bundle (required in non-interactive sessions: CI, scripts, redirected input, or --json)"
)]
yes: bool,
#[arg(
long = "arg",
value_name = "KEY=VALUE",
help = "Set a workflow config value (overrides [config] defaults). Repeatable."
)]
args: Vec<String>,
#[arg(
value_name = "KEY=VALUE",
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Direct config overrides: KEY=VALUE, --KEY=VALUE, or --KEY VALUE"
)]
config_overrides: Vec<String>,
#[arg(
long = "sample",
value_name = "SAMPLE",
help = "Add a sample to the run (repeatable, merges with all sources)"
)]
extra_samples: Vec<String>,
#[arg(long)]
ai_recover: bool,
#[arg(long = "ai-max-retries", value_name = "N")]
ai_max_retries: Option<u32>,
#[arg(
long = "samples",
value_name = "LIST",
conflicts_with = "extra_samples",
help = "Run only these samples: first:N (pilot), explicit names, or ready (complete inputs; repeatable, comma-separated)"
)]
samples_filter: Vec<String>,
#[arg(
long,
help = "Force re-execution of this run's rules (ignore up-to-date checks)"
)]
rerun: bool,
#[arg(long, help = "Skip the automatic report snapshot after the run")]
no_report_snapshot: bool,
},
Resume {
#[arg(
value_name = "CHECKPOINT",
help = "Path to the checkpoint file (.oxo-flow/checkpoint.json)"
)]
checkpoint: PathBuf,
#[arg(
short = 'j',
long,
default_value = "1",
help = "Maximum number of concurrent jobs"
)]
jobs: usize,
#[arg(long)]
ai_recover: bool,
#[arg(long = "ai-max-retries", value_name = "N")]
ai_max_retries: Option<u32>,
#[arg(
short = 'k',
long,
help = "Continue execution when a job fails (same semantics as `run`)"
)]
keep_going: bool,
#[arg(
long,
default_value = "0",
help = "Timeout per job in seconds (0 = disabled), or a duration like 1h/30m"
)]
timeout: String,
#[arg(
short = 'd',
long,
help = "Working directory to resume in (default: the one recorded in the checkpoint)"
)]
workdir: Option<PathBuf>,
#[arg(
long,
help = "Skip the automatic report snapshot after the resumed run"
)]
no_report_snapshot: bool,
},
DryRun {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: Option<PathBuf>,
#[arg(
short = 't',
long,
help = "Run only specific target rules (repeatable, prefix matching)"
)]
target: Vec<String>,
#[arg(long)]
ai: bool,
#[arg(long = "ai-max-retries", value_name = "N")]
ai_max_retries: Option<u32>,
#[arg(
long = "samples",
value_name = "LIST",
help = "Preview only these samples: first:N (pilot), explicit names, or ready (complete inputs; repeatable, comma-separated)"
)]
samples_filter: Vec<String>,
#[arg(
short = 'd',
long,
help = "Working directory to resolve paths against (default: the workflow file's directory)"
)]
workdir: Option<PathBuf>,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
skip_ref_build: bool,
#[arg(
long = "arg",
value_name = "KEY=VALUE",
help = "Set a workflow config value (overrides [config] defaults). Repeatable."
)]
args: Vec<String>,
#[arg(
value_name = "KEY=VALUE",
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Direct config overrides: KEY=VALUE, --KEY=VALUE, or --KEY VALUE"
)]
config_overrides: Vec<String>,
#[arg(
long = "sample",
value_name = "SAMPLE",
help = "Add a sample to the run (repeatable, merges with all sources)"
)]
extra_samples: Vec<String>,
#[arg(long)]
rerun: bool,
#[arg(long)]
resume_failed: bool,
},
Validate {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(
long,
help = "Validate as a sub-workflow fragment (skip DAG validation)"
)]
as_include: bool,
#[arg(long)]
ai: bool,
},
Init {
#[arg(value_name = "NAME", help = "Project name (no path separators)")]
name: String,
#[arg(short = 'd', long, help = "Target directory")]
dir: Option<PathBuf>,
},
Template {
#[arg(
value_name = "TEMPLATE",
help = "Template name or natural-language description (with --ai)"
)]
template: Option<String>,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
#[arg(long)]
ai: bool,
#[arg(long = "from-url", value_name = "URL")]
from_url: Vec<String>,
#[arg(long = "from-file", value_name = "PATH")]
from_file: Vec<PathBuf>,
#[arg(long = "ai-max-retries", value_name = "N")]
ai_max_retries: Option<u32>,
},
#[command(name = "ai")]
Ai {
#[arg(
value_name = "ACTION",
help = "Action to run: 'test' (comprehensive self-test), 'setup' (interactive wizard), or 'explain' (workflow explanation); omit for a quick status"
)]
action: Option<String>,
#[arg(value_name = "WORKFLOW")]
workflow: Option<PathBuf>,
#[arg(long, value_name = "RULE")]
step: Option<String>,
#[arg(long, value_enum, default_value_t = crate::commands::ai_explain::ExplainLevel::Beginner)]
level: crate::commands::ai_explain::ExplainLevel,
#[arg(long)]
json: bool,
},
Graph {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'f', long, default_value = "ascii", help = "Output format")]
format: String,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
#[arg(
long = "expanded",
help = "Show the DAG after wildcard/sample/scatter expansion (the actual runtime DAG)"
)]
expanded: bool,
},
Status {
#[arg(
value_name = "CHECKPOINT",
help = "Path to the checkpoint file (default: .oxo-flow/checkpoint.json)"
)]
checkpoint: Option<PathBuf>,
#[arg(long)]
timing: bool,
#[arg(
short = 'n',
long,
default_value = "10",
requires = "timing",
help = "Maximum number of rules to show in the --timing view"
)]
limit: usize,
},
Pull {
#[arg(
value_name = "URL",
help = "Bundle URL (gh:owner/repo@tag, https://, or file://)"
)]
url: String,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
},
Config {
#[command(subcommand)]
action: ConfigAction,
},
Diff {
#[arg(value_name = "WORKFLOW_A", help = "First workflow file to compare")]
workflow_a: PathBuf,
#[arg(value_name = "WORKFLOW_B", help = "Second workflow file to compare")]
workflow_b: PathBuf,
},
Debug {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(
short = 'r',
long = "rule",
help = "Show the expanded command for this rule only"
)]
rule_name: Option<String>,
#[arg(long)]
ai: bool,
},
Clean {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'n', long, help = "Preview which outputs would be deleted")]
dry_run: bool,
#[arg(
long,
help = "Actually delete outputs (without this flag, clean only previews)"
)]
force: bool,
#[arg(
long,
help = "Remove orphaned transform chunk directories (.oxo-flow/chunks)"
)]
orphans: bool,
#[arg(
short = 'd',
long,
help = "Working directory for .oxo-flow artifacts (default: the workflow file's directory)"
)]
workdir: Option<PathBuf>,
},
Env {
#[command(subcommand)]
action: EnvAction,
},
Format {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
#[arg(long, help = "Only check formatting, don't write")]
check: bool,
},
Lint {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(long, help = "Treat warnings as errors (non-zero exit on any warning)")]
strict: bool,
#[arg(long)]
ai: bool,
},
Touch {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'r', long = "rule", help = "Rule names to touch")]
rules: Vec<String>,
#[arg(
short = 'd',
long,
help = "Working directory the outputs live in (default: the workflow file's directory)"
)]
workdir: Option<PathBuf>,
},
Report {
#[arg(
value_name = "WORKFLOW",
help = "Path to the .oxoflow workflow file (auto-discovered when omitted)"
)]
workflow: Option<PathBuf>,
#[arg(
short = 'f',
long,
help = "Output format: html, json, md, pdf, pdf-command (default: html, or inferred from the -o extension)"
)]
format: Option<String>,
#[arg(short = 'o', long, help = "Output file path ('-' for stdout)")]
output: Option<PathBuf>,
#[arg(
long = "checkpoint",
value_name = "PATH",
help = "Path to checkpoint file (default: .oxo-flow/checkpoint.json)"
)]
checkpoint_path: Option<PathBuf>,
#[arg(
long = "ai",
help = "AI result interpretation โ plain-language summary of execution outcomes, caveats, and next steps (stderr + report section)"
)]
ai: bool,
#[arg(
short = 'd',
long,
help = "Working directory to look for .oxo-flow in (default: the workflow file's directory)"
)]
workdir: Option<PathBuf>,
#[arg(
long,
help = "Reproducible output: pin the generation timestamp (SOURCE_DATE_EPOCH or the Unix epoch) so identical state yields byte-identical reports"
)]
ci: bool,
#[arg(long, help = "Omit the generation timestamp from the report")]
no_timestamps: bool,
#[arg(
long,
help = "Fail (exit 2) when the checkpoint is missing or the report template fails to render"
)]
strict: bool,
#[arg(long, help = "List available report sections and exit")]
list_sections: bool,
#[arg(
long = "run",
value_name = "DIR",
conflicts_with = "workflow",
help = "Workdir of a previous run: the workflow and checkpoint are auto-discovered there"
)]
run_dir: Option<PathBuf>,
#[arg(long, help = "Failure-focused report: diagnosis first")]
failed: bool,
#[arg(
long,
help = "Template-only report โ ignore execution data (no checkpoint required)"
)]
plan: bool,
#[arg(
long = "init-template",
help = "Write the built-in report template to ./report-template.tera and exit"
)]
init_template: bool,
#[arg(
long = "list-templates",
help = "List available report templates and exit"
)]
list_templates: bool,
#[arg(
long = "r-data",
value_name = "DIR",
help = "Write R-friendly TSV files (sample_table.tsv, metrics.tsv) to DIR"
)]
r_data: Option<PathBuf>,
#[arg(
long = "diff",
value_name = "CHECKPOINT",
help = "Model-level diff of this report's checkpoint against another checkpoint (stderr, terminal-highlighted)"
)]
diff: Option<PathBuf>,
#[arg(
long = "acct",
value_name = "PATH",
help = "Import sacct-style CSV accounting (JobID,JobName,State,Elapsed,CPUTime,MaxRSS) into a Resource Accounting section"
)]
acct: Option<PathBuf>,
},
Serve {
#[arg(long, default_value = "personal", env = "OXO_FLOW_MODE")]
mode: String,
#[arg(
long,
default_value = "127.0.0.1",
env = "OXO_FLOW_HOST",
help = "Address to bind"
)]
host: String,
#[arg(
short = 'p',
long,
default_value = "8080",
env = "OXO_FLOW_PORT",
help = "Port to listen on"
)]
port: u16,
#[arg(
long,
default_value = "/",
env = "OXO_FLOW_BASE_PATH",
help = "Base URL path for the web interface"
)]
base_path: String,
#[arg(long = "open", env = "OXO_FLOW_OPEN_BROWSER")]
open_browser: bool,
},
Completions {
#[arg(value_enum, help = "Shell to generate completions for")]
shell: clap_complete::Shell,
},
Export {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'f', long, default_value = "docker", help = "Output format")]
format: String,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
},
Cluster {
#[command(subcommand)]
action: ClusterAction,
},
Batch {
#[arg(
value_name = "TEMPLATE",
help = "Shell command template with {item} placeholder"
)]
template: String,
#[arg(
value_name = "ITEMS",
help = "Files or items to process (glob patterns supported)"
)]
items: Vec<String>,
#[arg(
short = 'j',
long,
default_value = "1",
help = "Maximum number of concurrent jobs"
)]
jobs: usize,
#[arg(short = 'x', long, help = "Stop on the first failed item")]
stop_on_error: bool,
#[arg(short = 'f', long, help = "Read items from a file (one per line)")]
file: Option<PathBuf>,
#[arg(long = "json-output", help = "Output results as formatted JSON")]
json_output: bool,
#[arg(
short = 'n',
long,
help = "Preview the commands without executing them"
)]
dry_run: bool,
#[arg(short = 'd', long, help = "Working directory for execution")]
workdir: Option<PathBuf>,
#[arg(short = 'e', long, help = "Environment to run each item in")]
environment: Option<String>,
#[arg(long, help = "Record output checksums for later verification")]
checksum: bool,
#[arg(long, help = "Generate a .oxoflow workflow file from the template")]
generate_workflow: bool,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
},
Provenance {
#[command(subcommand)]
action: ProvenanceAction,
},
Schema,
Test {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(long, help = "File whose existence is verified after the test run")]
output: Option<PathBuf>,
#[arg(
long,
help = "Execute the workflow (default only validates and verifies outputs)"
)]
run: bool,
#[arg(
short = 'j',
long,
default_value = "1",
help = "Maximum number of concurrent jobs"
)]
jobs: usize,
#[arg(
long = "samples",
value_name = "LIST",
help = "Test only these samples: first:N (pilot), explicit names, or ready (complete inputs; repeatable, comma-separated)"
)]
samples_filter: Vec<String>,
#[arg(
long,
help = "Run deep checks: script files, env YAML files, backend binaries, reference data"
)]
deep: bool,
#[arg(
short = 'd',
long,
help = "Working directory for the test run (default: the workflow file's directory)"
)]
workdir: Option<PathBuf>,
#[arg(long)]
profile: Option<String>,
#[arg(
short = 't',
long,
help = "Run only specific target rules (repeatable, prefix matching) โ applies to the --run step"
)]
target: Vec<String>,
#[arg(
long,
default_value = "0",
help = "Timeout per job in seconds (0 = disabled) โ applies to the --run step"
)]
timeout: String,
#[arg(
long,
default_value = "0",
help = "Number of times to retry failed jobs โ applies to the --run step"
)]
retry: u32,
#[arg(
short = 'k',
long,
help = "Continue execution when a job fails โ applies to the --run step"
)]
keep_going: bool,
},
Publish {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'o', long, help = "Output file path")]
output: Option<PathBuf>,
#[arg(long, help = "Generate conda lockfiles for reproducible environments")]
with_lockfiles: bool,
#[arg(
long = "format",
help = "Bundle archive format: tar.zst (default) or tar.gz"
)]
format: Option<String>,
},
License {
#[arg(value_name = "LICENSE_PATH", help = "License file path")]
path: Option<PathBuf>,
},
}
#[derive(Subcommand, Debug)]
pub enum EnvAction {
List {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: Option<PathBuf>,
},
Check {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: Option<PathBuf>,
},
Create {
#[arg(
value_name = "SPEC",
help = "Environment spec file (.yaml/.yml/.toml/.lock), or a description with --ai"
)]
spec: PathBuf,
#[arg(short = 'n', long, help = "Environment or profile name")]
name: Option<String>,
#[arg(
long = "ai",
help = "Generate the environment spec from a natural-language description (SPEC is the description)"
)]
ai: bool,
#[arg(
long = "backend",
default_value = "conda",
help = "Environment backend to generate: conda (YAML) or pixi (TOML)"
)]
backend: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ConfigAction {
Show {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
},
#[command(alias = "check")]
Stats {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
},
Get {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(value_name = "KEY", help = "Config key")]
key: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ClusterAction {
Submit {
#[arg(value_name = "WORKFLOW", help = "Path to the .oxoflow workflow file")]
workflow: PathBuf,
#[arg(short = 'b', long, help = "Cluster backend: slurm, pbs, sge, or lsf")]
backend: String,
#[arg(short = 'q', long, help = "Cluster queue or partition name")]
queue: Option<String>,
#[arg(short = 'a', long, help = "Cluster billing account")]
account: Option<String>,
#[arg(
long,
value_name = "SPEC",
help = "Wall-time limit for every job (24h, 2d, or 24:00:00); a rule's time_limit wins"
)]
walltime: Option<String>,
#[arg(
long = "extra-arg",
value_name = "ARG",
allow_hyphen_values = true,
help = "Extra scheduler argument, passed through verbatim (repeatable)"
)]
extra_args: Vec<String>,
#[arg(
short = 'o',
long,
default_value = "cluster_scripts",
help = "Output file path"
)]
output: PathBuf,
#[arg(
short = 't',
long,
help = "Run only specific target rules (repeatable, prefix matching)"
)]
target: Vec<String>,
#[arg(long, help = "Generate scripts without submitting")]
dry_run: bool,
#[arg(long, help = "Generate job scripts with dependency support")]
with_dependencies: bool,
},
Status {
#[arg(short = 'b', long, help = "Cluster backend: slurm, pbs, sge, or lsf")]
backend: String,
#[arg(value_name = "JOB_IDS", help = "Job ID(s)")]
job_ids: Vec<String>,
},
Cancel {
#[arg(short = 'b', long, help = "Cluster backend: slurm, pbs, sge, or lsf")]
backend: String,
#[arg(value_name = "JOB_IDS", help = "Job ID(s)")]
job_ids: Vec<String>,
},
Logs {
#[arg(short = 'b', long, help = "Cluster backend: slurm, pbs, sge, or lsf")]
backend: String,
#[arg(value_name = "JOB_ID", help = "Cluster job ID")]
job_id: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ProvenanceAction {
Verify {
#[arg(value_name = "CHECKPOINT_PATH", help = "Path to the checkpoint file")]
checkpoint: PathBuf,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let use_color = std::io::IsTerminal::is_terminal(&std::io::stdout())
&& std::env::var_os("NO_COLOR").is_none()
&& !std::env::args_os().any(|arg| arg == "--no-color");
let matches = {
let mut command = Cli::command();
if let Some(cfg) = oxo_flow_web::config::load() {
if let Some(mode) = cfg.server.mode {
let mode = clap::builder::OsStr::from(mode);
command = command
.mut_subcommand("serve", |c| c.mut_arg("mode", |a| a.default_value(mode)));
}
if let Some(host) = cfg.server.host {
let host = clap::builder::OsStr::from(host);
command = command
.mut_subcommand("serve", |c| c.mut_arg("host", |a| a.default_value(host)));
}
if let Some(port) = cfg.server.port {
let port = clap::builder::OsStr::from(port.to_string());
command = command
.mut_subcommand("serve", |c| c.mut_arg("port", |a| a.default_value(port)));
}
if let Some(base_path) = cfg.server.base_path {
let base_path = clap::builder::OsStr::from(base_path);
command = command.mut_subcommand("serve", |c| {
c.mut_arg("base_path", |a| a.default_value(base_path))
});
}
}
command = command.help_template(if use_color {
banner::HELP_TEMPLATE
} else {
banner::HELP_TEMPLATE_PLAIN
});
if !use_color {
command = command.color(clap::ColorChoice::Never);
}
command.get_matches()
};
let cli = Cli::from_arg_matches(&matches)?;
if cli.no_color || std::env::var_os("NO_COLOR").is_some() {
colored::control::set_override(false);
}
let default_level = if cli.quiet {
"error"
} else if cli.verbose {
"debug"
} else {
"info"
};
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_level)),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
crate::commands::set_quiet_mode(cli.quiet);
match cli.command {
Commands::Run {
workflow,
jobs,
keep_going,
workdir,
target,
retry,
timeout,
resume_failed,
profile,
max_threads,
max_memory,
skip_env_setup,
skip_ref_build,
cache_dir,
provenance,
bundle,
yes,
args,
config_overrides,
extra_samples,
ai_recover,
ai_max_retries,
samples_filter,
rerun,
no_report_snapshot,
} => {
use anyhow::Context as _;
use colored::Colorize as _;
#[allow(unused_imports)]
use std::io::BufRead as _;
let (wf, wd) = if let Some(bundle_path) = bundle {
let (extracted_wf, extracted_dir) =
crate::commands::bundle::extract_and_verify_bundle(&bundle_path)?;
let effective_wd = workdir.unwrap_or_else(|| extracted_dir.clone());
if !yes {
let manifest_path =
crate::commands::bundle::find_manifest_in_dir(&extracted_dir)?;
let manifest_json = std::fs::read_to_string(&manifest_path)
.context("failed to read bundle manifest")?;
let manifest: serde_json::Value =
serde_json::from_str(&manifest_json).context("failed to parse manifest")?;
eprintln!();
eprintln!("{}", "Bundle Verification Complete".bold().green());
eprintln!(
" Workflow: {}",
manifest["workflow"].as_str().unwrap_or("unknown")
);
eprintln!(
" Format: {}",
manifest["format"].as_str().unwrap_or("unknown")
);
eprintln!(
" Version: {}",
manifest["oxo_flow_version"].as_str().unwrap_or("unknown")
);
if let Some(resources) = manifest.get("resources")
&& let Some(recommendations) = resources.get("recommendations")
{
eprintln!(" Resources:");
if let Some(t) = recommendations["min_threads"].as_u64() {
eprintln!(" Min threads: {}", t.to_string().cyan());
}
if let Some(m) = recommendations["min_memory_mb"].as_u64() {
eprintln!(
" Min memory: {} MB ({:.1} GB)",
m.to_string().cyan(),
m as f64 / 1024.0
);
}
if let Some(g) = recommendations["min_gpu"].as_u64()
&& g > 0
{
eprintln!(" Min GPU: {}", g.to_string().cyan());
}
}
eprintln!(" Source: {}", bundle_path.display());
let can_prompt = crate::commands::bundle::can_prompt_for_confirmation(
cli.json,
std::io::IsTerminal::is_terminal(&std::io::stderr()),
std::io::IsTerminal::is_terminal(&std::io::stdin()),
);
if !can_prompt {
let _ = std::fs::remove_dir_all(&extracted_dir);
anyhow::bail!(
"Running a bundle requires confirmation, and this session cannot prompt for it. \
Use --yes to confirm in CI, scripts, or with --json.\n\
Bundle: {}",
bundle_path.display()
);
}
eprintln!();
eprint!(" {} Proceed with execution? [y/N] ", "โ ".yellow());
use std::io::Write as _;
std::io::stderr().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y")
&& !input.trim().eq_ignore_ascii_case("yes")
{
let _ = std::fs::remove_dir_all(&extracted_dir);
anyhow::bail!("execution cancelled by user");
}
}
(Some(extracted_wf), Some(effective_wd))
} else {
(workflow, workdir)
};
let mut merged_args = config_overrides;
merged_args.extend(args);
run_command(
wf,
jobs,
keep_going,
wd,
target,
retry,
timeout,
resume_failed,
profile,
max_threads,
max_memory,
skip_env_setup,
skip_ref_build,
cache_dir,
provenance,
cli.json,
merged_args,
extra_samples,
ai_recover,
ai_max_retries,
samples_filter,
rerun,
no_report_snapshot,
)
.await?
}
Commands::Resume {
checkpoint,
jobs,
ai_recover,
ai_max_retries,
keep_going,
timeout,
workdir,
no_report_snapshot,
} => {
resume_command(
checkpoint,
jobs,
ai_recover,
ai_max_retries,
keep_going,
timeout,
workdir,
no_report_snapshot,
)
.await?
}
Commands::DryRun {
workflow,
target,
ai,
ai_max_retries,
samples_filter,
workdir,
profile,
skip_ref_build,
args,
config_overrides,
extra_samples,
rerun,
resume_failed,
} => {
let mut merged_args = config_overrides;
merged_args.extend(args);
dry_run_command(
workflow,
target,
cli.verbose,
cli.json,
ai,
ai_max_retries,
samples_filter,
workdir,
profile,
skip_ref_build,
merged_args,
extra_samples,
rerun,
resume_failed,
)
.await?
}
Commands::Validate {
workflow,
as_include,
ai,
} => {
validate_command(workflow, as_include, cli.json, ai).await?;
}
Commands::Init { name, dir } => init_command(name, dir)?,
Commands::Template {
template,
output,
ai,
from_url,
from_file,
ai_max_retries,
} => template_command(template, output, ai, from_url, from_file, ai_max_retries).await?,
Commands::Ai {
action,
workflow,
step,
level,
json,
} => {
let explain_args = workflow.is_some() || step.is_some() || json;
match action.as_deref() {
Some("explain") => {
let Some(workflow) = workflow else {
anyhow::bail!(
"'ai explain' requires a workflow file:\n oxo-flow ai explain <workflow.oxoflow> [--step <rule>] [--level beginner|expert] [--json]"
);
};
crate::commands::ai_explain::ai_explain_command(
&workflow,
step.as_deref(),
level,
json,
)
.await?
}
Some("test") => {
if explain_args {
anyhow::bail!("'ai test' takes no workflow/--step/--json arguments");
}
ai_test_command().await?
}
Some("setup") => {
if explain_args {
anyhow::bail!("'ai setup' takes no workflow/--step/--json arguments");
}
ai_setup_command().await?
}
None => {
if explain_args {
anyhow::bail!(
"workflow/--step/--json require the 'explain' action:\n oxo-flow ai explain <workflow.oxoflow>"
);
}
ai_status_command().await?
}
Some(other) => {
anyhow::bail!(
"unknown ai action '{other}' โ expected one of: test, setup, explain"
)
}
}
}
Commands::Graph {
workflow,
format,
output,
expanded,
} => handle_graph(workflow, format, output, expanded)?,
Commands::Status {
checkpoint,
timing,
limit,
} => handle_status(checkpoint, cli.json, timing, limit).await?,
Commands::Pull { url, output } => crate::commands::pull::pull_command(&url, output).await?,
Commands::Config { action } => crate::commands::infra::handle_config(action)?,
Commands::Diff {
workflow_a,
workflow_b,
} => handle_diff(workflow_a, workflow_b)?,
Commands::Debug {
workflow,
rule_name,
ai,
} => debug_command(workflow, rule_name, ai).await?,
Commands::Clean {
workflow,
dry_run,
force,
orphans,
workdir,
} => clean_command(workflow, dry_run, force, orphans, workdir)?,
Commands::Env { action } => env_command(action).await?,
Commands::Format {
workflow,
output,
check,
} => format_command(workflow, output, check)?,
Commands::Lint {
workflow,
strict,
ai,
} => lint_command(workflow, strict, cli.json, ai).await?,
Commands::Touch {
workflow,
rules,
workdir,
} => touch_command(workflow, rules, workdir)?,
Commands::Report {
workflow,
format,
output,
checkpoint_path,
ai,
workdir,
ci,
no_timestamps,
strict,
list_sections,
run_dir,
failed,
plan,
init_template,
list_templates,
r_data,
diff,
acct,
} => {
handle_report(crate::commands::output::ReportArgs {
workflow,
format,
output,
checkpoint_path,
ai,
workdir,
ci,
no_timestamps,
strict,
list_sections,
run_dir,
failed,
plan,
init_template,
list_templates,
r_data,
diff,
acct,
})
.await?
}
Commands::Serve {
mode,
host,
port,
base_path,
open_browser,
} => crate::commands::web::handle_serve(mode, host, port, base_path, open_browser).await?,
Commands::Completions { shell } => handle_completions(shell)?,
Commands::Export {
workflow,
format,
output,
} => handle_export(workflow, format, output)?,
Commands::Cluster { action } => cluster_command(action).await?,
Commands::Batch {
template,
items,
jobs,
stop_on_error,
file,
json_output: json,
dry_run,
workdir,
environment,
checksum,
generate_workflow,
output,
} => {
batch_command(
template,
items,
jobs,
stop_on_error,
file,
cli.json || json,
dry_run,
workdir,
environment,
checksum,
generate_workflow,
output,
)
.await?
}
Commands::Provenance { action } => match action {
ProvenanceAction::Verify { checkpoint } => provenance_verify_command(checkpoint)?,
},
Commands::Schema => {
let schema = include_str!("../schema/oxoflow-v1.schema.json");
println!("{schema}");
}
Commands::Test {
workflow,
output,
run,
jobs,
samples_filter,
deep,
workdir,
profile,
target,
timeout,
retry,
keep_going,
} => {
use colored::Colorize;
eprintln!(
"{} Running test suite for {}\n",
"๐งช".bold(),
workflow.display()
);
eprintln!("{} Validation...", "1.".bold());
validate_command(workflow.clone(), false, cli.json, false).await?;
eprintln!("{} Lint...", "2.".bold());
lint_command(workflow.clone(), false, cli.json, false).await?;
eprintln!("{} Dry-run...", "3.".bold());
dry_run_command(
Some(workflow.clone()),
target.clone(),
cli.verbose,
cli.json,
false,
None,
samples_filter.clone(),
workdir.clone(),
profile.clone(),
false,
vec![],
vec![],
false,
false,
)
.await?;
if deep {
eprintln!("{} Deep checks...", "4.".bold());
deep_check_command(&workflow, workdir.as_deref(), cli.json)?;
}
if run {
eprintln!("{} Execution...", if deep { "5." } else { "4." }.bold());
run_command(
Some(workflow),
jobs,
keep_going, workdir.clone(), target.clone(), retry, timeout.clone(), false, profile.clone(), 0, 0, false, false, None, false, cli.json,
vec![], vec![], false, None, samples_filter.clone(),
false, false, )
.await?;
}
if let Some(output_path) = output {
if output_path.exists() {
eprintln!(
"{} Output file exists: {}",
"โ".green().bold(),
output_path.display()
);
} else {
eprintln!(
"{} Output file not found: {}",
"โ".red().bold(),
output_path.display()
);
std::process::exit(1);
}
}
eprintln!("\n{} All checks passed.", "โ".green().bold());
}
Commands::Publish {
workflow,
output,
with_lockfiles,
format,
} => publish_command(workflow, output, with_lockfiles, format)?,
Commands::License { path } => handle_license(path)?,
}
Ok(())
}