use std::path::Path;
use anyhow::anyhow;
use crate::engine::wave_config::{default_local_home, read_wave_home};
use crate::engine::wave_context::{
resolve_managed_wave_name_sync, resolve_run_wave_name, WaveResolveError,
};
use crate::engine::wave_home::{
HomeActionDto, HomeRuntimeDto, HomeState, WaveHome, HOME_ROUTED_ENV, WAVE_HOME_ENV,
};
use crate::lf::commands::util::find_repo_root;
use crate::lf::{Commands, HomeCommand};
use crate::provider_account::lease::AccountSelection;
pub fn run(cmd: &HomeCommand, repo: &Path) -> anyhow::Result<()> {
match cmd {
HomeCommand::Probe { wave, json } => probe_cmd(wave.as_deref(), *json, repo),
HomeCommand::Start { wave, json } => start_cmd(wave.as_deref(), *json, repo),
}
}
fn resolve_wave_name(wave: Option<&str>) -> anyhow::Result<String> {
if let Some(name) = wave.and_then(crate::ops::util::normalize_wave_name) {
return Ok(name);
}
resolve_managed_wave_name_sync(None).map_err(|error| match error {
WaveResolveError::NoContext => {
anyhow!("no wave given and none in context; pass a wave name")
}
other => other.into(),
})
}
fn probe_cmd(wave: Option<&str>, json: bool, repo: &Path) -> anyhow::Result<()> {
let name = resolve_managed_wave_name_sync(wave).map_err(|err| anyhow!("{err}"))?;
let home = read_wave_home(repo, &name);
let rt = tokio::runtime::Runtime::new()?;
let runtime = rt.block_on(crate::ops::home::probe_home(&name, &home, repo));
if json {
println!("{}", serde_json::to_string(&runtime)?);
} else {
print_runtime(&name, &runtime);
}
Ok(())
}
fn start_cmd(wave: Option<&str>, json: bool, repo: &Path) -> anyhow::Result<()> {
let name = resolve_wave_name(wave)?;
let home = read_wave_home(repo, &name);
let rt = tokio::runtime::Runtime::new()?;
let result = rt
.block_on(crate::ops::home::start_home(&name, &home, repo))
.map_err(|error| anyhow!(error))?;
if json {
println!("{}", serde_json::to_string(&result)?);
} else {
let verb = if result.started {
"started"
} else {
"already running"
};
println!("{name} {verb} on {}", result.runtime.home.address);
print_runtime(&name, &result.runtime);
}
Ok(())
}
fn print_runtime(name: &str, runtime: &HomeRuntimeDto) {
let state = match runtime.state {
HomeState::Unreachable => "unreachable",
HomeState::Stopped => "stopped",
HomeState::Running => "running",
HomeState::Unknown => "unknown",
};
let action = match &runtime.action {
HomeActionDto::Attach { endpoint } => format!("Attach ({endpoint})"),
HomeActionDto::Start { home } => format!("Start on {home}"),
HomeActionDto::Reason { message } => message.clone(),
};
println!("{name} {} [{state}]", runtime.home.address);
println!(" reason {}", runtime.reason);
println!(" action {action}");
}
pub fn route(
command: &Commands,
wave: Option<&str>,
account_selection: &AccountSelection,
args: &[String],
) -> Option<anyhow::Result<()>> {
if !is_routable(command) {
return None;
}
if std::env::var_os(HOME_ROUTED_ENV).is_some() {
return None;
}
let home = resolve_home(wave);
let dest = home.ssh_destination()?;
Some(crate::lf::commands::ssh::run_routed(
&dest,
home.ssh_port(),
None,
account_selection,
&remote_argv(args),
))
}
fn is_routable(command: &Commands) -> bool {
matches!(
command,
Commands::Pr { .. }
| Commands::Commit { .. }
| Commands::Rebase { .. }
| Commands::Release { .. }
| Commands::Pm { .. }
)
}
fn resolve_home(wave: Option<&str>) -> WaveHome {
if let Ok(raw) = std::env::var(WAVE_HOME_ENV) {
if let Some(home) = WaveHome::parse(&raw) {
return home;
}
}
let repo = find_repo_root();
match (
wave.map(str::to_string).or_else(resolve_run_wave_name),
&repo,
) {
(Some(name), Ok(repo)) => read_wave_home(repo, &name),
_ => match &repo {
Ok(repo) => default_local_home(repo),
Err(_) => WaveHome::local("user").expect("literal 'user' is a valid Home owner"),
},
}
}
fn remote_argv(args: &[String]) -> Vec<String> {
let mut cmd = Vec::with_capacity(args.len());
cmd.push("lf".to_string());
let mut args = args.iter().skip(1);
while let Some(arg) = args.next() {
if arg == "--" {
cmd.push(arg.clone());
cmd.extend(args.cloned());
break;
}
if matches!(arg.as_str(), "--account" | "--only-account") {
let _ = args.next();
continue;
}
if arg.starts_with("--account=") || arg.starts_with("--only-account=") {
continue;
}
cmd.push(arg.clone());
}
cmd
}
#[cfg(test)]
mod tests {
use super::{is_routable, remote_argv};
use crate::lf::Commands;
#[test]
fn remote_argv_swaps_the_binary_path_for_bare_lf() {
let args = vec![
"/Users/jack/src/loopflow/target/debug/lf".to_string(),
"pr".to_string(),
"open".to_string(),
];
assert_eq!(remote_argv(&args), vec!["lf", "pr", "open"]);
}
#[test]
fn remote_argv_consumes_account_selectors_at_the_origin_boundary() {
let args = vec![
"/local/lf",
"--account",
"claude=personal",
"--only-account=codex=reserve",
"commit",
"-m",
"ship it",
"--",
"--account",
"literal",
]
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>();
assert_eq!(
remote_argv(&args),
vec![
"lf",
"commit",
"-m",
"ship it",
"--",
"--account",
"literal"
]
);
}
#[test]
fn routable_set_is_repo_and_release_ops_only() {
assert!(is_routable(&Commands::Commit {
message: None,
push: false,
no_add: false,
}));
assert!(is_routable(&Commands::Pr { cmd: None }));
assert!(!is_routable(&Commands::Status {
wave: None,
json: false,
}));
assert!(!is_routable(&Commands::Stop {
name: "infra".to_string(),
}));
}
}