use clap::Parser;
use tracing::debug;
use tracing_subscriber::EnvFilter;
use loopflow::journal::{self, LfEventFields, LfEventType, LfNode};
use loopflow::lf::{Cli, Commands};
const VALUE_FLAGS: &[&str] = &[
"-d",
"--direction",
"-a",
"--area",
"-m",
"--model",
"-w",
"--wave",
];
const BOOL_FLAGS: &[&str] = &[
"-l",
"--list",
"-c",
"--clipboard",
"--no-direction",
"--yolo",
"-i",
"--interactive",
"-b",
"--batch",
"--tui",
"--ide",
"--chrome",
"--no-chrome",
"--lfdocs",
"--no-lfdocs",
"--diff-files",
"--no-diff-files",
"--diff",
"--no-diff",
"-h",
"--help",
"-V",
"--version",
];
const KNOWN_COMMANDS: &[&str] = &[":", "op", "help"];
fn is_value_flag(arg: &str) -> bool {
VALUE_FLAGS.contains(&arg)
}
fn is_known_flag(arg: &str) -> bool {
BOOL_FLAGS.contains(&arg) || is_value_flag(arg)
}
fn reorder_args(args: Vec<String>) -> Vec<String> {
if args.len() <= 1 {
return args;
}
let program = args[0].clone();
let rest = &args[1..];
if let Some(first) = rest.first() {
if KNOWN_COMMANDS.contains(&first.as_str()) {
return args;
}
}
let mut flags_before: Vec<String> = Vec::new();
let mut step_and_args: Vec<String> = Vec::new();
let mut flags_after: Vec<String> = Vec::new();
let mut i = 0;
let mut found_step = false;
while i < rest.len() {
let arg = &rest[i];
if !found_step {
if arg.starts_with('-') {
flags_before.push(arg.clone());
if is_value_flag(arg) && i + 1 < rest.len() {
i += 1;
flags_before.push(rest[i].clone());
}
} else {
found_step = true;
step_and_args.push(arg.clone());
}
} else {
if arg.starts_with('-') {
if is_known_flag(arg) {
flags_after.push(arg.clone());
if is_value_flag(arg) && i + 1 < rest.len() {
i += 1;
flags_after.push(rest[i].clone());
}
} else {
step_and_args.push(arg.clone());
}
} else {
step_and_args.push(arg.clone());
}
}
i += 1;
}
let mut result = vec![program];
result.extend(flags_before);
result.extend(flags_after);
result.extend(step_and_args);
result
}
fn join_args(args: &[String]) -> Option<String> {
if args.is_empty() {
None
} else {
Some(args.join(" "))
}
}
fn with_runtime<T>(
repo_root: &std::path::Path,
command: &[String],
run: impl FnOnce() -> anyhow::Result<T>,
) -> anyhow::Result<T> {
journal::emit(
repo_root,
LfNode::Run,
LfEventType::Started,
LfEventFields {
wave_name: loopflow::engine::worktrees::main_repo_root(repo_root)
.ok()
.and_then(|main_repo| {
loopflow::engine::worktrees::wave_name_from_worktree_and_main(
repo_root, &main_repo,
)
}),
worktree: Some(repo_root.display().to_string()),
command: Some(command.to_vec()),
..LfEventFields::default()
},
);
let result = run();
match &result {
Ok(_) => journal::emit(
repo_root,
LfNode::Run,
LfEventType::Completed,
LfEventFields::default(),
),
Err(err) => journal::emit(
repo_root,
LfNode::Run,
LfEventType::Errored,
LfEventFields {
error: Some(err.to_string()),
..LfEventFields::default()
},
),
}
result
}
fn in_repo_runtime<T>(
command: &[String],
run: impl FnOnce(&std::path::Path) -> anyhow::Result<T>,
) -> anyhow::Result<T> {
let repo_root = loopflow::lf::commands::util::find_repo_root()?;
with_runtime(&repo_root, command, || run(&repo_root))
}
fn with_step_runtime<T>(
repo_root: &std::path::Path,
step_name: &str,
run: impl FnOnce() -> anyhow::Result<T>,
) -> anyhow::Result<T> {
journal::emit(
repo_root,
LfNode::Step,
LfEventType::Started,
LfEventFields {
step: Some(step_name.to_string()),
index: Some(0),
..LfEventFields::default()
},
);
let result = run();
match &result {
Ok(_) => journal::emit(
repo_root,
LfNode::Step,
LfEventType::Completed,
LfEventFields {
step: Some(step_name.to_string()),
index: Some(0),
..LfEventFields::default()
},
),
Err(err) => journal::emit(
repo_root,
LfNode::Step,
LfEventType::Errored,
LfEventFields {
step: Some(step_name.to_string()),
index: Some(0),
error: Some(err.to_string()),
..LfEventFields::default()
},
),
}
result
}
fn run_target(
name: &str,
message: Option<&str>,
cli: &Cli,
command: &[String],
) -> anyhow::Result<()> {
let repo_root = loopflow::lf::commands::util::find_repo_root()?;
match loopflow::lf::discovery::discover_target(&repo_root, name)? {
loopflow::lf::discovery::Target::Step(_) => with_runtime(&repo_root, command, || {
with_step_runtime(&repo_root, name, || {
loopflow::lf::commands::run::run(Some(name), message, cli)?;
let options = loopflow::ops::CommitOptions {
add: true,
message: Some(format!("lf commit: {name}")),
..loopflow::ops::CommitOptions::for_task(name)
};
loopflow::ops::commit_workflow(&repo_root, &options, &loopflow::ops::NullProgress)?;
Ok(())
})
}),
loopflow::lf::discovery::Target::Flow(flow) => with_runtime(&repo_root, command, || {
loopflow::lf::commands::flow::run(&flow, message, cli, &repo_root)
}),
}
}
fn main() -> anyhow::Result<()> {
ctrlc::set_handler(|| {
loopflow::engine::agent::kill_child_if_running();
std::process::exit(130);
})
.expect("failed to set Ctrl+C handler");
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("lf=info,loopflow=info"));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.without_time()
.init();
let args: Vec<String> = std::env::args().collect();
let args = reorder_args(args);
let cli = Cli::parse_from(args.clone());
debug!(?cli, "parsed CLI arguments");
if cli.list {
return in_repo_runtime(&args, |_| loopflow::lf::commands::list::show_all());
}
match &cli.command {
Some(Commands::Inline { prompt }) => {
let text = prompt.join(" ");
in_repo_runtime(&args, |_| {
loopflow::lf::commands::run::run(None, Some(&text), &cli)
})
}
Some(Commands::Op { op }) => in_repo_runtime(&args, |_| {
loopflow::lf::commands::ops::run(op, cli.model.as_deref())
}),
Some(Commands::External(external_args)) => {
let (name, step_args) = loopflow::lf::commands::run::split_step_args(external_args)?;
let message = join_args(&step_args);
run_target(&name, message.as_deref(), &cli, &args)
}
None => in_repo_runtime(&args, |_| {
loopflow::lf::commands::run::run(None, None, &cli)
}),
}
}
#[cfg(test)]
mod tests {
use super::reorder_args;
#[test]
fn reorder_args_flag_after_step() {
let args = vec!["lf".to_string(), "debug".to_string(), "-c".to_string()];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-c", "debug"]);
}
#[test]
fn reorder_args_flag_before_step() {
let args = vec!["lf".to_string(), "-c".to_string(), "debug".to_string()];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-c", "debug"]);
}
#[test]
fn reorder_args_value_flag_before_step() {
let args = vec![
"lf".to_string(),
"-m".to_string(),
"codex".to_string(),
"implement".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-m", "codex", "implement"]);
}
#[test]
fn reorder_args_value_flag_after_step() {
let args = vec![
"lf".to_string(),
"debug".to_string(),
"-m".to_string(),
"codex".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-m", "codex", "debug"]);
}
#[test]
fn reorder_args_mixed_flags() {
let args = vec![
"lf".to_string(),
"-i".to_string(),
"implement".to_string(),
"-c".to_string(),
"-m".to_string(),
"claude".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-i", "-c", "-m", "claude", "implement"]);
}
#[test]
fn reorder_args_no_direction_flag_after_step() {
let args = vec![
"lf".to_string(),
"implement".to_string(),
"--no-direction".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "--no-direction", "implement"]);
}
#[test]
fn reorder_args_step_with_args() {
let args = vec![
"lf".to_string(),
"implement:".to_string(),
"add".to_string(),
"logout".to_string(),
"-c".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-c", "implement:", "add", "logout"]);
}
#[test]
fn reorder_args_known_command_unchanged() {
let args = vec![
"lf".to_string(),
"op".to_string(),
"commit".to_string(),
"-m".to_string(),
"msg".to_string(),
];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "op", "commit", "-m", "msg"]);
}
#[test]
fn reorder_args_no_step() {
let args = vec!["lf".to_string(), "-l".to_string()];
let result = reorder_args(args);
assert_eq!(result, vec!["lf", "-l"]);
}
}