use anyhow::{Context, Result};
use colored::Colorize;
use std::path::{Path, PathBuf};
pub fn discover_workflow_file() -> Result<PathBuf> {
let cwd = std::env::current_dir().context("cannot determine current directory")?;
discover_workflow_file_in(&cwd)
}
pub fn discover_workflow_file_in(dir: &Path) -> Result<PathBuf> {
let main_workflow = dir.join("main.oxoflow");
if main_workflow.exists() {
return Ok(main_workflow);
}
let mut oxoflow_files: Vec<PathBuf> = Vec::new();
for entry in std::fs::read_dir(dir)
.with_context(|| format!("cannot read directory {}", dir.display()))?
{
let entry = entry.context("cannot read directory entry")?;
let path = entry.path();
if let Some(ext) = path.extension()
&& ext == "oxoflow"
{
oxoflow_files.push(path);
}
}
if oxoflow_files.is_empty() {
return Err(anyhow::anyhow!(
"no .oxoflow file found in {}.\n\
The repository must contain a workflow file (main.oxoflow or any *.oxoflow).",
dir.display()
));
}
oxoflow_files.sort();
Ok(oxoflow_files.into_iter().next().unwrap())
}
pub fn resolve_workflow(provided: Option<PathBuf>) -> Result<PathBuf> {
match provided {
Some(path) => Ok(path),
None => discover_workflow_file(),
}
}
static QUIET_MODE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn set_quiet_mode(quiet: bool) {
QUIET_MODE.store(quiet, std::sync::atomic::Ordering::Relaxed);
}
pub fn print_banner() {
if QUIET_MODE.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
eprintln!(
"{} v{} — {}",
"oxo-flow".bold().cyan(),
env!("CARGO_PKG_VERSION"),
"Rust-native bioinformatics pipeline engine".dimmed()
);
eprintln!("{}", env!("CARGO_PKG_REPOSITORY").cyan());
}
pub fn expand_batch_template(template: &str, item: &str, nr: usize) -> String {
let path = std::path::Path::new(item);
let basename = path
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default();
let stem = path
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let ext = path
.extension()
.map(|e| e.to_string_lossy())
.unwrap_or_default();
let dir = oxo_flow_core::parent_dir(path).to_string_lossy();
template
.replace("{}", item)
.replace("{item}", item)
.replace("{nr}", &nr.to_string())
.replace("{basename}", &basename)
.replace("{stem}", &stem)
.replace("{ext}", &ext)
.replace("{dir}", &dir)
}
pub fn parse_item_lines(content: &str) -> Vec<String> {
content
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.map(|line| line.trim().to_string())
.collect()
}
pub fn collect_batch_items(items: &[String], file: Option<&PathBuf>) -> Result<Vec<String>> {
if let Some(path) = file {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read items from {}", path.display()))?;
return Ok(parse_item_lines(&content));
}
if items.is_empty() {
use std::io::{self, BufRead};
let stdin = io::stdin();
let lines: Vec<String> = stdin.lock().lines().map_while(Result::ok).collect();
if !lines.is_empty() {
return Ok(parse_item_lines(&lines.join("\n")));
}
return Err(anyhow::anyhow!(
"no items provided (use -f FILE, stdin, or arguments)"
));
}
let expanded: Vec<String> = items
.iter()
.flat_map(|item| {
if item.contains('*') || item.contains('?') || item.contains('[') {
glob::glob(item)
.ok()
.into_iter()
.flatten()
.filter_map(|p| p.ok())
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
} else {
vec![item.clone()]
}
})
.collect();
Ok(expanded)
}
pub fn wrap_batch_command(cmd: &str, env_spec: &str) -> String {
if let Some((type_, spec)) = env_spec.split_once(':') {
match type_.trim() {
"conda" => {
let env_name = std::path::Path::new(spec.trim())
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(spec.trim());
format!("conda run --no-banner -n {} {}", env_name, cmd)
}
"docker" => format!("docker run --rm {} sh -c '{}'", spec.trim(), cmd),
"singularity" => format!("singularity exec {} sh -c '{}'", spec.trim(), cmd),
_ => cmd.to_string(),
}
} else {
format!("conda run --no-banner -n {} {}", env_spec.trim(), cmd)
}
}
pub fn run_batch_command(cmd: &str, workdir: &Path) -> Result<i32> {
use std::process::Command;
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(workdir)
.output()
.with_context(|| format!("failed to execute: {}", cmd))?;
Ok(output.status.code().unwrap_or(-1))
}
pub mod ai_check;
pub mod ai_explain;
pub mod ai_recover;
pub mod ai_runtime;
pub mod ai_session;
pub mod ai_status;
pub mod ai_template;
pub mod batch;
pub mod bundle;
pub mod clean;
pub mod cluster;
pub mod completions;
pub mod config_comments;
pub mod info;
pub mod infra;
pub mod output;
pub mod project;
pub mod provenance;
pub mod publish;
pub mod pull;
pub mod quality;
pub mod run;
pub mod run_cluster;
pub mod run_preview;
pub mod samples;
pub mod web;