Refactor fabro-cli run command layout

This commit is contained in:
Bryan Helmkamp 2026-03-27 10:26:59 -04:00
parent 241f0192f2
commit c871ecb274
No known key found for this signature in database
22 changed files with 196 additions and 162 deletions

View file

@ -596,13 +596,7 @@ pub(crate) struct UpgradeArgs {
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// LLM prompt operations
#[command(hide = true)]
Llm(LlmNamespace),
/// Run an agentic coding session
#[command(hide = true)]
Exec(fabro_agent::cli::AgentArgs),
pub(crate) enum RunCommands {
/// Launch a workflow run
Run(RunArgs),
/// Create a workflow run (allocate run dir, persist spec)
@ -618,8 +612,8 @@ pub(crate) enum Commands {
run: String,
},
/// Internal: run the engine process (reads run.json from run dir)
#[command(name = "_run_engine", hide = true)]
RunEngine {
#[command(name = "__detached", hide = true)]
Detached {
/// Path to the run directory
#[arg(long)]
run_dir: PathBuf,
@ -627,6 +621,76 @@ pub(crate) enum Commands {
#[arg(long)]
resume: bool,
},
/// Get a preview URL for a port on a run's sandbox
Preview(PreviewArgs),
/// SSH into a run's Daytona sandbox
Ssh(SshArgs),
/// Show the diff of changes from a workflow run
#[command(hide = true)]
Diff(DiffArgs),
/// View the event log of a workflow run
Logs(LogsArgs),
/// Resume an interrupted workflow run
Resume(ResumeArgs),
/// Rewind a workflow run to an earlier checkpoint
Rewind(RewindArgs),
/// Fork a workflow run from an earlier checkpoint into a new run
Fork(ForkArgs),
/// Block until a workflow run completes
Wait(WaitArgs),
}
impl RunCommands {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Run(_) => "run",
Self::Create(_) => "create",
Self::Start { .. } => "start",
Self::Attach { .. } => "attach",
Self::Detached { .. } => "__detached",
Self::Preview(_) => "preview",
Self::Ssh(_) => "ssh",
Self::Diff(_) => "diff",
Self::Logs(_) => "logs",
Self::Resume(_) => "resume",
Self::Rewind(_) => "rewind",
Self::Fork(_) => "fork",
Self::Wait(_) => "wait",
}
}
}
#[derive(Subcommand)]
pub(crate) enum RunsCommands {
/// List workflow runs
#[command(hide = true)]
Ps(RunsListArgs),
/// Remove one or more workflow runs
Rm(RunsRemoveArgs),
/// Show detailed information about a workflow run
Inspect(InspectArgs),
}
impl RunsCommands {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Ps(_) => "ps",
Self::Rm(_) => "rm",
Self::Inspect(_) => "inspect",
}
}
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// LLM prompt operations
#[command(hide = true)]
Llm(LlmNamespace),
/// Run an agentic coding session
#[command(hide = true)]
Exec(fabro_agent::cli::AgentArgs),
#[command(flatten)]
RunCmd(RunCommands),
/// Validate a workflow
Validate(ValidateArgs),
/// Render a workflow graph as SVG or PNG
@ -638,17 +702,8 @@ pub(crate) enum Commands {
Asset(AssetNamespace),
/// Copy files to/from a run's sandbox
Cp(CpArgs),
/// Get a preview URL for a port on a run's sandbox
Preview(PreviewArgs),
/// SSH into a run's Daytona sandbox
Ssh(SshArgs),
/// Show the diff of changes from a workflow run
#[command(hide = true)]
Diff(DiffArgs),
/// View the event log of a workflow run
Logs(LogsArgs),
/// Show detailed information about a workflow run
Inspect(InspectArgs),
#[command(flatten)]
RunsCmd(RunsCommands),
/// List and test LLM models
Model {
#[command(subcommand)]
@ -676,11 +731,6 @@ pub(crate) enum Commands {
#[arg(long, default_value = "http://localhost:5173")]
web_url: String,
},
/// List workflow runs
#[command(hide = true)]
Ps(RunsListArgs),
/// Remove one or more workflow runs
Rm(RunsRemoveArgs),
/// Pull request operations
Pr(PrNamespace),
/// Skill management
@ -688,14 +738,6 @@ pub(crate) enum Commands {
Skill(SkillNamespace),
/// Manage secrets in ~/.fabro/.env
Secret(SecretNamespace),
/// Resume an interrupted workflow run
Resume(ResumeArgs),
/// Rewind a workflow run to an earlier checkpoint
Rewind(RewindArgs),
/// Fork a workflow run from an earlier checkpoint into a new run
Fork(ForkArgs),
/// Block until a workflow run completes
Wait(WaitArgs),
/// Workflow operations
Workflow(WorkflowNamespace),
/// Open the Discord community in the browser
@ -736,20 +778,12 @@ impl Commands {
AssetCommand::Cp(_) => "asset cp",
},
Self::Exec(_) => "exec",
Self::Run(_) => "run",
Self::Create(_) => "create",
Self::Start { .. } => "start",
Self::Attach { .. } => "attach",
Self::RunEngine { .. } => "_run_engine",
Self::RunCmd(cmd) => cmd.name(),
Self::Validate(_) => "validate",
Self::Graph(_) => "graph",
Self::Parse(_) => "parse",
Self::Cp(_) => "cp",
Self::Preview(_) => "preview",
Self::Ssh(_) => "ssh",
Self::Diff(_) => "diff",
Self::Logs(_) => "logs",
Self::Inspect(_) => "inspect",
Self::RunsCmd(cmd) => cmd.name(),
Self::Model { command } => match command {
Some(fabro_llm::cli::ModelsCommand::List { .. }) => "model list",
Some(fabro_llm::cli::ModelsCommand::Test { .. }) => "model test",
@ -764,8 +798,6 @@ impl Commands {
},
Self::Init => "init",
Self::Install { .. } => "install",
Self::Ps(_) => "ps",
Self::Rm(_) => "rm",
Self::Pr(ns) => match &ns.command {
PrCommand::Create(_) => "pr create",
PrCommand::List(_) => "pr list",
@ -779,10 +811,6 @@ impl Commands {
SecretCommand::Rm(_) => "secret rm",
SecretCommand::Set(_) => "secret set",
},
Self::Resume(_) => "resume",
Self::Rewind(_) => "rewind",
Self::Fork(_) => "fork",
Self::Wait(_) => "wait",
Self::Workflow(ns) => match &ns.command {
WorkflowCommand::List(_) => "workflow list",
WorkflowCommand::Create(_) => "workflow create",

View file

@ -1,35 +1,20 @@
pub mod asset;
pub mod attach;
pub mod cp;
pub mod create;
pub(crate) mod detached_support;
pub mod diff;
pub mod doctor;
pub mod exec;
pub mod fork;
pub mod graph;
pub mod inspect;
pub mod install;
pub mod llm;
pub mod logs;
pub mod model;
pub mod parse;
pub mod pr;
pub mod preview;
pub mod provider;
pub mod repo;
pub mod resume;
pub mod rewind;
pub mod run;
pub mod run_engine;
pub(crate) mod run_progress;
pub mod runs;
pub mod secret;
pub mod skill;
pub mod ssh;
pub mod start;
pub mod system;
pub mod upgrade;
pub mod validate;
pub mod wait;
pub mod workflow;

View file

@ -5,7 +5,7 @@ use fabro_sandbox::SandboxProvider;
use crate::args::RunArgs;
use super::run::{
use super::execute::{
apply_execution_overrides, cached_graph_path, default_run_dir, load_workflow_source_input,
parse_labels, print_diagnostics_from_error, print_workflow_report_from_persisted,
resolve_sandbox_provider, write_run_config_snapshot, ExecutionOverrides,

View file

@ -47,7 +47,7 @@ pub async fn execute(run_dir: PathBuf, resume: bool) -> Result<()> {
}
let result = if resume {
super::run::resume_from_record(
super::execute::resume_from_record(
persisted,
run_dir.clone(),
cli_config,
@ -57,7 +57,7 @@ pub async fn execute(run_dir: PathBuf, resume: bool) -> Result<()> {
)
.await
} else {
super::run::run_from_record(
super::execute::run_from_record(
persisted,
run_dir.clone(),
cli_config,

View file

@ -1281,7 +1281,7 @@ pub fn print_run_summary(run_dir: &Path, run_id: &str, styles: &Styles) {
return;
};
// PR info from pull_request.json (saved by _run_engine)
// PR info from pull_request.json (saved by __detached)
let pr_url = std::fs::read_to_string(run_dir.join("pull_request.json"))
.ok()
.and_then(|content| {

View file

@ -0,0 +1,81 @@
use anyhow::Result;
use crate::args::{GlobalArgs, RunCommands};
pub(crate) mod attach;
pub(crate) mod create;
pub(crate) mod detached;
pub(crate) mod detached_support;
pub(crate) mod diff;
pub(crate) mod execute;
pub(crate) mod fork;
pub(crate) mod logs;
pub(crate) mod preview;
pub(crate) mod resume;
pub(crate) mod rewind;
pub(crate) mod run_progress;
pub(crate) mod ssh;
pub(crate) mod start;
pub(crate) mod wait;
pub async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<()> {
match cmd {
RunCommands::Run(args) => execute::execute(args, globals).await,
RunCommands::Create(args) => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let cli_config = crate::cli_config::load_cli_config(None)?;
let (run_id, _run_dir) = create::create_run(&args, cli_config, styles, true).await?;
println!("{run_id}");
Ok(())
}
RunCommands::Start { run } => {
let base = fabro_workflows::run_lookup::default_runs_base();
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
let child = start::start_run(&run_info.path, false)?;
eprintln!("Started engine process (PID {})", child.id());
Ok(())
}
RunCommands::Attach { run } => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let base = fabro_workflows::run_lookup::default_runs_base();
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
let exit_code = attach::attach_run(&run_info.path, false, styles, None).await?;
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}
Ok(())
}
RunCommands::Detached { run_dir, resume } => detached::execute(run_dir, resume).await,
RunCommands::Preview(args) => preview::run(args).await,
RunCommands::Ssh(args) => ssh::run(args).await,
RunCommands::Diff(args) => diff::run(args).await,
RunCommands::Logs(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
logs::run(args, &styles)
}
RunCommands::Resume(args) => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let cli_config = crate::cli_config::load_cli_config(None)?;
fabro_beastie::guard(cli_config.prevent_idle_sleep_enabled())
};
resume::resume_command(args, styles).await
}
RunCommands::Rewind(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
rewind::run(&args, &styles)
}
RunCommands::Fork(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
fork::run(&args, &styles)
}
RunCommands::Wait(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
wait::run(args, &styles)
}
}
}

View file

@ -73,7 +73,7 @@ pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow
println!("{run_id}");
} else {
let exit_code = super::attach::attach_run(&run_dir, true, styles, Some(child)).await?;
super::run::print_run_summary(&run_dir, &run_id, styles);
super::execute::print_run_summary(&run_dir, &run_id, styles);
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}

View file

@ -56,7 +56,7 @@ pub fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Child> {
return Err(err);
}
};
cmd.args(["_run_engine", "--run-dir"]).arg(run_dir);
cmd.args(["__detached", "--run-dir"]).arg(run_dir);
if resume {
cmd.arg("--resume");
}

View file

@ -7,9 +7,22 @@ use cli_table::{print_stdout, Cell, CellStruct, Color, Style, Table};
use fabro_util::terminal::Styles;
use tracing::warn;
use crate::args::{RunsListArgs, RunsRemoveArgs};
use crate::args::{RunsCommands, RunsListArgs, RunsRemoveArgs};
use crate::shared::{color_if, format_duration_ms, tilde_path};
pub(crate) mod inspect;
pub async fn dispatch(cmd: RunsCommands) -> Result<()> {
match cmd {
RunsCommands::Ps(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
list_command(&args, &styles)
}
RunsCommands::Rm(args) => remove_command(&args).await,
RunsCommands::Inspect(args) => inspect::run(&args),
}
}
pub fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
let base = fabro_workflows::run_lookup::default_runs_base();
let runs = fabro_workflows::run_lookup::scan_runs(&base)?;

View file

@ -140,8 +140,7 @@ async fn main_inner() -> (String, Result<()>) {
let upgrade_handle = if matches!(
command.as_ref(),
Commands::Run(_)
| Commands::Create(_)
Commands::RunCmd(RunCommands::Run(_) | RunCommands::Create(_))
| Commands::Exec(_)
| Commands::Repo(_)
| Commands::Init
@ -156,35 +155,7 @@ async fn main_inner() -> (String, Result<()>) {
match *command {
Commands::Llm(ns) => commands::llm::dispatch(ns, &globals).await?,
Commands::Exec(args) => commands::exec::execute(args, &globals).await?,
Commands::Run(args) => commands::run::execute(args, &globals).await?,
Commands::Create(args) => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let cli_config = cli_config::load_cli_config(None)?;
let (run_id, _run_dir) =
commands::create::create_run(&args, cli_config, styles, true).await?;
println!("{run_id}");
}
Commands::Start { run } => {
let base = fabro_workflows::run_lookup::default_runs_base();
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
let child = commands::start::start_run(&run_info.path, false)?;
eprintln!("Started engine process (PID {})", child.id());
}
Commands::Attach { run } => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
let base = fabro_workflows::run_lookup::default_runs_base();
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
let exit_code =
commands::attach::attach_run(&run_info.path, false, styles, None).await?;
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}
}
Commands::RunEngine { run_dir, resume } => {
commands::run_engine::execute(run_dir, resume).await?;
}
Commands::RunCmd(cmd) => commands::run::dispatch(cmd, &globals).await?,
Commands::Validate(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::validate::run(&args, &styles)?;
@ -200,22 +171,7 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Cp(args) => {
commands::cp::cp_command(args).await?;
}
Commands::Preview(args) => {
commands::preview::run(args).await?;
}
Commands::Ssh(args) => {
commands::ssh::run(args).await?;
}
Commands::Diff(args) => {
commands::diff::run(args).await?;
}
Commands::Logs(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
commands::logs::run(args, &styles)?;
}
Commands::Inspect(args) => {
commands::inspect::run(&args)?;
}
Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd).await?,
Commands::Model { command } => commands::model::execute(command, &globals).await?,
#[cfg(feature = "server")]
Commands::Serve(args) => {
@ -246,37 +202,8 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Install { web_url } => {
commands::install::run_install(&web_url).await?;
}
Commands::Ps(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
commands::runs::list_command(&args, &styles)?;
}
Commands::Rm(args) => {
commands::runs::remove_command(&args).await?;
}
Commands::Pr(ns) => commands::pr::dispatch(ns).await?,
Commands::Secret(ns) => commands::secret::dispatch(ns)?,
Commands::Resume(args) => {
let styles: &'static fabro_util::terminal::Styles =
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let cli_config = cli_config::load_cli_config(None)?;
fabro_beastie::guard(cli_config.prevent_idle_sleep_enabled())
};
commands::resume::resume_command(args, styles).await?;
}
Commands::Rewind(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::rewind::run(&args, &styles)?;
}
Commands::Fork(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::fork::run(&args, &styles)?;
}
Commands::Wait(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::wait::run(args, &styles)?;
}
Commands::Workflow(ns) => commands::workflow::dispatch(ns)?,
Commands::Skill(ns) => commands::skill::dispatch(ns)?,
Commands::Upgrade(args) => {
@ -358,7 +285,7 @@ mod tests {
let cli = Cli::try_parse_from(["fabro", "create", "my-workflow.toml", "--goal", "test"])
.expect("should parse");
match *cli.command {
Commands::Create(args) => {
Commands::RunCmd(RunCommands::Create(args)) => {
assert_eq!(
args.workflow.as_deref(),
Some(std::path::Path::new("my-workflow.toml"))
@ -373,7 +300,7 @@ mod tests {
fn parse_start_command() {
let cli = Cli::try_parse_from(["fabro", "start", "ABC123"]).expect("should parse");
match *cli.command {
Commands::Start { run } => {
Commands::RunCmd(RunCommands::Start { run }) => {
assert_eq!(run, "ABC123");
}
_ => panic!("unexpected command variant"),
@ -384,7 +311,7 @@ mod tests {
fn parse_attach_command() {
let cli = Cli::try_parse_from(["fabro", "attach", "ABC123"]).expect("should parse");
match *cli.command {
Commands::Attach { run } => {
Commands::RunCmd(RunCommands::Attach { run }) => {
assert_eq!(run, "ABC123");
}
_ => panic!("unexpected command variant"),
@ -392,11 +319,11 @@ mod tests {
}
#[test]
fn parse_run_engine_command() {
let cli = Cli::try_parse_from(["fabro", "_run_engine", "--run-dir", "/tmp/runs/test"])
fn parse_detached_command() {
let cli = Cli::try_parse_from(["fabro", "__detached", "--run-dir", "/tmp/runs/test"])
.expect("should parse");
match *cli.command {
Commands::RunEngine { run_dir, resume } => {
Commands::RunCmd(RunCommands::Detached { run_dir, resume }) => {
assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test"));
assert!(!resume);
}
@ -405,17 +332,17 @@ mod tests {
}
#[test]
fn parse_run_engine_with_resume() {
fn parse_detached_with_resume() {
let cli = Cli::try_parse_from([
"fabro",
"_run_engine",
"__detached",
"--run-dir",
"/tmp/runs/test",
"--resume",
])
.expect("should parse");
match *cli.command {
Commands::RunEngine { run_dir, resume } => {
Commands::RunCmd(RunCommands::Detached { run_dir, resume }) => {
assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test"));
assert!(resume);
}

View file

@ -880,11 +880,11 @@ fn start_by_workflow_name_prefers_newly_created_submitted_run() {
);
}
// Bug 2: _run_engine should use cached graph.fabro, not run.json working_directory.
// Bug 2: __detached should use cached graph.fabro, not run.json working_directory.
// When the original workflow file is deleted between create and start,
// the engine should read the snapshot saved at create time.
#[test]
fn bug2_run_engine_uses_cached_graph_not_original_path() {
fn bug2_detached_uses_cached_graph_not_original_path() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path().join("run");
std::fs::create_dir_all(&run_dir).unwrap();
@ -928,9 +928,9 @@ digraph G {
// The cached graph snapshot saved by `fabro create`
std::fs::write(run_dir.join("graph.fabro"), dot).unwrap();
// _run_engine should use graph.fabro and never reference the deleted file.
// __detached should use graph.fabro and never reference the deleted file.
let output = arc()
.args(["_run_engine", "--run-dir", run_dir.to_str().unwrap()])
.args(["__detached", "--run-dir", run_dir.to_str().unwrap()])
.env("NO_COLOR", "1")
.timeout(std::time::Duration::from_secs(15))
.output()
@ -945,7 +945,7 @@ digraph G {
}
#[test]
fn bug4_run_engine_resume_rejects_completed_run_without_mutating_it() {
fn bug4_detached_resume_rejects_completed_run_without_mutating_it() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let workflow_path = project.path().join("workflow.fabro");
@ -1005,7 +1005,7 @@ digraph Test {
arc()
.env("HOME", home.path())
.args(["_run_engine", "--run-dir", &run_dir, "--resume"])
.args(["__detached", "--run-dir", &run_dir, "--resume"])
.timeout(std::time::Duration::from_secs(10))
.assert()
.failure()