Add global JSON output mode

This commit is contained in:
Bryan Helmkamp 2026-03-31 07:51:19 -04:00
parent c148a693b9
commit e99ebfa3f2
125 changed files with 1762 additions and 426 deletions

View file

@ -17,6 +17,10 @@ pub(crate) const LONG_VERSION: &str = concat!(
#[derive(Args)]
pub(crate) struct GlobalArgs {
/// Output as JSON
#[arg(long, global = true, env = "FABRO_JSON", value_parser = clap::builder::BoolishValueParser::new())]
pub json: bool,
/// Enable DEBUG-level logging (default is INFO)
#[arg(long, global = true, env = "FABRO_DEBUG", value_parser = clap::builder::BoolishValueParser::new())]
pub debug: bool,
@ -48,6 +52,13 @@ pub(crate) struct GlobalArgs {
pub server_url: Option<String>,
}
impl GlobalArgs {
pub(crate) fn require_no_json(&self) -> anyhow::Result<()> {
anyhow::ensure!(!self.json, "--json is not supported for this command");
Ok(())
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum CliSandboxProvider {
Local,
@ -188,10 +199,6 @@ pub(crate) struct RunsListArgs {
#[command(flatten)]
pub(crate) filter: RunFilterArgs,
/// Output as JSON
#[arg(long)]
pub(crate) json: bool,
/// Show all runs, not just running (like docker ps -a)
#[arg(short = 'a', long)]
pub(crate) all: bool,
@ -313,10 +320,6 @@ pub(crate) struct AssetListArgs {
/// Filter to assets from a specific retry attempt
#[arg(long)]
pub(crate) retry: Option<u32>,
/// Output as JSON
#[arg(long)]
pub(crate) json: bool,
}
#[derive(Args)]
@ -495,10 +498,6 @@ pub(crate) struct WaitArgs {
/// Poll interval in milliseconds
#[arg(long, value_name = "MS", default_value = "1000")]
pub(crate) interval: u64,
/// Output conclusion as JSON
#[arg(long)]
pub(crate) json: bool,
}
#[derive(Args)]

View file

@ -7,7 +7,7 @@ use fabro_workflow::assets::{AssetEntry, scan_assets};
use fabro_workflow::run_lookup::{resolve_run, runs_base};
use crate::args::{AssetCpArgs, GlobalArgs};
use crate::shared::split_run_path;
use crate::shared::{print_json_pretty, split_run_path};
use crate::user_config::load_user_settings_with_globals;
pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()> {
@ -64,10 +64,20 @@ pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()>
dest_file.display()
)
})?;
println!("Copied {} to {}", entry.relative_path, dest_file.display());
if globals.json {
print_json_pretty(&serde_json::json!({
"copied": [{
"relative_path": entry.relative_path,
"destination": dest_file.display().to_string(),
}],
}))?;
} else {
println!("Copied {} to {}", entry.relative_path, dest_file.display());
}
return Ok(());
}
let mut copied = Vec::new();
if args.tree {
for entry in &entries {
let relative_dest = PathBuf::from(&entry.node_slug)
@ -84,6 +94,10 @@ pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()>
dest_file.display()
)
})?;
copied.push(serde_json::json!({
"relative_path": entry.relative_path,
"destination": dest_file.display().to_string(),
}));
}
} else {
let mut by_filename: Vec<(String, &AssetEntry)> = Vec::with_capacity(entries.len());
@ -116,14 +130,22 @@ pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()>
dest_file.display()
)
})?;
copied.push(serde_json::json!({
"relative_path": entry.relative_path,
"destination": dest_file.display().to_string(),
}));
}
}
println!(
"Copied {} asset(s) to {}",
entries.len(),
args.dest.display()
);
if globals.json {
print_json_pretty(&serde_json::json!({ "copied": copied }))?;
} else {
println!(
"Copied {} asset(s) to {}",
entries.len(),
args.dest.display()
);
}
Ok(())
}

View file

@ -19,7 +19,7 @@ pub(super) fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> Result
args.retry,
)?;
if args.json {
if globals.json {
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}

View file

@ -2,6 +2,7 @@ use std::io::Write;
use std::path::Path;
use crate::args::{GlobalArgs, SettingsArgs};
use crate::shared::print_json_pretty;
use crate::user_config;
use fabro_config::{ConfigLayer, FabroSettings};
@ -18,6 +19,10 @@ fn merged_config(workflow: Option<&Path>, globals: &GlobalArgs) -> anyhow::Resul
pub(crate) fn execute(args: &SettingsArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
let config = merged_config(args.workflow.as_deref(), globals)?;
if globals.json {
print_json_pretty(&config)?;
return Ok(());
}
let mut yaml = serde_yaml::to_string(&config)?;
if !yaml.ends_with('\n') {
yaml.push('\n');

View file

@ -22,6 +22,8 @@ use regex::Regex;
#[cfg(feature = "server")]
use semver::Version;
use crate::args::GlobalArgs;
use crate::shared::print_json_pretty;
use crate::user_config::load_user_settings;
// ---------------------------------------------------------------------------
@ -949,17 +951,25 @@ async fn probe_url(http: &reqwest::Client, url: &str) -> Result<(), String> {
.map_err(|e| e.to_string())
}
pub(crate) async fn run_doctor(verbose: bool, live: bool) -> i32 {
pub(crate) async fn run_doctor(
verbose: bool,
live: bool,
globals: &GlobalArgs,
) -> Result<i32, anyhow::Error> {
let styles = Styles::detect_stdout();
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.expect("valid template")
.tick_strings(&["", "", "", "", "", "", "", "", "", "", ""]),
);
spinner.set_message("Running checks…");
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
let spinner = if globals.json {
None
} else {
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.expect("valid template")
.tick_strings(&["", "", "", "", "", "", "", "", "", "", ""]),
);
spinner.set_message("Running checks…");
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
Some(spinner)
};
// Gather state
let cli_settings = load_user_settings().unwrap_or_default();
@ -1209,15 +1219,21 @@ pub(crate) async fn run_doctor(verbose: bool, live: bool) -> i32 {
sections,
};
spinner.finish_and_clear();
if let Some(spinner) = spinner {
spinner.finish_and_clear();
}
let term_width = console::Term::stderr().size().1;
print!(
"{}",
report.render(&styles, verbose, None, Some(term_width))
);
if globals.json {
print_json_pretty(&report)?;
} else {
let term_width = console::Term::stderr().size().1;
print!(
"{}",
report.render(&styles, verbose, None, Some(term_width))
);
}
i32::from(report.has_errors())
Ok(i32::from(report.has_errors()))
}
// ---------------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
use anyhow::Result;
#[cfg(feature = "server")]
use fabro_agent::cli::run_with_args_and_client;
use fabro_agent::cli::{AgentArgs, run_with_args};
use fabro_agent::cli::{AgentArgs, OutputFormat, run_with_args};
use fabro_config::mcp::McpServerEntry;
use fabro_mcp::config::McpServerConfig;
@ -19,6 +19,9 @@ pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result
exec_defaults.and_then(|a| a.permissions),
exec_defaults.and_then(|a| a.output_format),
);
if globals.json {
args.output_format = Some(OutputFormat::Json);
}
#[cfg(feature = "server")]
let resolved = user_config::resolve_mode(
globals.storage_dir.as_deref(),

View file

@ -11,13 +11,19 @@ use fabro_validate::Severity;
use fabro_workflow::operations::{ValidateInput, WorkflowInput, validate};
use tracing::debug;
use crate::args::{GraphArgs, GraphDirection};
use crate::shared::{print_diagnostics, read_workflow_file, relative_path};
use crate::args::{GlobalArgs, GraphArgs, GraphDirection};
use crate::shared::{
absolute_or_current, print_diagnostics, print_json_pretty, read_workflow_file, relative_path,
};
static RANKDIR_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"rankdir\s*=\s*\w+").unwrap());
pub(crate) fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
pub(crate) fn run(args: &GraphArgs, styles: &Styles, globals: &GlobalArgs) -> anyhow::Result<()> {
if globals.json && args.output.is_none() {
globals.require_no_json()?;
}
let cwd = std::env::current_dir()?;
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
.combine(ConfigLayer::user()?)
@ -43,6 +49,12 @@ pub(crate) fn run(args: &GraphArgs, styles: &Styles) -> anyhow::Result<()> {
if let Some(ref output_path) = args.output {
std::fs::write(output_path, &rendered)?;
if globals.json {
print_json_pretty(&serde_json::json!({
"path": absolute_or_current(output_path),
"format": args.format.to_string(),
}))?;
}
} else {
std::io::stdout().write_all(&rendered)?;
}

View file

@ -22,6 +22,7 @@ use tokio::sync::oneshot;
use tokio::task::spawn_blocking;
use super::doctor;
use crate::args::GlobalArgs;
use crate::shared::provider_auth::{
prompt_and_validate_key, prompt_confirm, provider_display_name, run_openai_oauth_or_api_key,
write_env_file,
@ -486,7 +487,8 @@ async fn setup_github_app(
Ok(env_pairs)
}
pub(crate) async fn run_install(web_url: &str) -> Result<()> {
pub(crate) async fn run_install(web_url: &str, globals: &GlobalArgs) -> Result<()> {
globals.require_no_json()?;
let s = Styles::detect_stderr();
let emoji = console::Emoji("⚒️ ", "");
@ -759,7 +761,7 @@ pub(crate) async fn run_install(web_url: &str) -> Result<()> {
// Reload .env so doctor sees the values we just wrote
let _ = dotenvy::from_path(&env_path);
eprintln!();
doctor::run_doctor(true, true).await;
let _ = doctor::run_doctor(true, true, globals).await?;
}
eprintln!();

View file

@ -11,6 +11,7 @@ pub(super) async fn execute(
cli_settings: &FabroSettings,
globals: &GlobalArgs,
) -> Result<()> {
globals.require_no_json()?;
let llm_defaults = cli_settings.llm.as_ref();
if args.model.is_none() {
args.model = llm_defaults.and_then(|l| l.model.clone());

View file

@ -30,10 +30,10 @@ pub(super) async fn execute(
client,
base_url: resolved.server_base_url,
};
run_prompt_via_server(args, &server).await?;
run_prompt_via_server(args, &server, globals.json).await?;
}
crate::user_config::ExecutionMode::Standalone => {
run_prompt(args).await?;
run_prompt(args, globals.json).await?;
}
}
}
@ -41,7 +41,7 @@ pub(super) async fn execute(
#[cfg(not(feature = "server"))]
{
let _ = globals;
run_prompt(args).await?;
run_prompt(args, globals.json).await?;
}
Ok(())

View file

@ -35,5 +35,5 @@ pub(crate) async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs
}
};
run_models(command, server).await
run_models(command, server, globals.json).await
}

View file

@ -3,10 +3,11 @@ use std::io::Write;
use fabro_config::project::resolve_workflow;
use fabro_graphviz::parser::parse_ast;
use crate::args::ParseArgs;
use crate::args::{GlobalArgs, ParseArgs};
use crate::shared::read_workflow_file;
pub(crate) fn run(args: &ParseArgs) -> anyhow::Result<()> {
pub(crate) fn run(args: &ParseArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
let _ = globals;
let stdout = std::io::stdout();
run_to(args, stdout.lock())
}

View file

@ -6,6 +6,7 @@ use fabro_workflow::run_lookup::runs_base;
use tracing::info;
use crate::args::{GlobalArgs, PrCloseArgs};
use crate::shared::print_json_pretty;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn close_command(
@ -15,13 +16,14 @@ pub(super) async fn close_command(
) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
close_from(&base, args, github_app).await
close_from(&base, args, github_app, globals).await
}
async fn close_from(
base: &Path,
args: PrCloseArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let (record, _run_dir) = super::load_pr_record(base, &args.run_id).await?;
@ -40,7 +42,14 @@ async fn close_from(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request");
println!("Closed #{} ({})", record.number, record.html_url);
if globals.json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,
}))?;
} else {
println!("Closed #{} ({})", record.number, record.html_url);
}
Ok(())
}

View file

@ -13,6 +13,7 @@ use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, PrCreateArgs};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -23,13 +24,14 @@ pub(super) async fn create_command(
) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
create_from(&base, args, github_app).await
create_from(&base, args, github_app, globals).await
}
async fn create_from(
base: &Path,
args: PrCreateArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let storage_dir = base.parent().unwrap_or(base);
let store = store::build_store(storage_dir)?;
@ -149,10 +151,18 @@ async fn create_from(
if let Err(err) = record.save(&run_dir.join("pull_request.json")) {
tracing::warn!(error = %err, "Failed to save pull_request.json");
}
println!("{}", record.html_url);
if globals.json {
print_json_pretty(&record)?;
} else {
println!("{}", record.html_url);
}
}
None => {
println!("No pull request created (empty diff).");
if globals.json {
print_json_pretty(&serde_json::Value::Null)?;
} else {
println!("No pull request created (empty diff).");
}
}
}

View file

@ -5,12 +5,23 @@ use fabro_config::FabroSettingsExt;
use fabro_workflow::pull_request::PullRequestRecord;
use fabro_workflow::run_lookup::{runs_base, scan_runs_combined};
use futures::future::join_all;
use serde::Serialize;
use tracing::info;
use crate::args::{GlobalArgs, PrListArgs};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
#[derive(Serialize)]
struct PrRow {
run_id: String,
number: u64,
state: String,
title: String,
url: String,
}
pub(super) async fn list_command(
args: PrListArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
@ -19,7 +30,7 @@ pub(super) async fn list_command(
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
list_from(store.as_ref(), &base, args, github_app).await
list_from(store.as_ref(), &base, args, github_app, globals).await
}
async fn list_from(
@ -27,15 +38,8 @@ async fn list_from(
base: &Path,
args: PrListArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
struct PrRow {
run_id: String,
number: u64,
state: String,
title: String,
url: String,
}
let creds = github_app.context(
"GitHub App credentials required — set GITHUB_APP_PRIVATE_KEY and configure app_id",
)?;
@ -55,6 +59,10 @@ async fn list_from(
}
if entries.is_empty() {
if globals.json {
print_json_pretty(&Vec::<PrRow>::new())?;
return Ok(());
}
println!("No pull requests found.");
return Ok(());
}
@ -111,6 +119,11 @@ async fn list_from(
.collect()
};
if globals.json {
print_json_pretty(&rows)?;
return Ok(());
}
if rows.is_empty() {
println!("No open pull requests found. Use --all to include closed/merged.");
return Ok(());

View file

@ -7,6 +7,7 @@ use tracing::info;
use fabro_workflow::run_lookup::runs_base;
use crate::args::{GlobalArgs, PrMergeArgs};
use crate::shared::print_json_pretty;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn merge_command(
@ -16,13 +17,14 @@ pub(super) async fn merge_command(
) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
merge_from(&base, args, github_app).await
merge_from(&base, args, github_app, globals).await
}
async fn merge_from(
base: &Path,
args: PrMergeArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let (record, _run_dir) = super::load_pr_record(base, &args.run_id).await?;
@ -42,7 +44,15 @@ async fn merge_from(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request");
println!("Merged #{} ({})", record.number, record.html_url);
if globals.json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,
"method": args.method,
}))?;
} else {
println!("Merged #{} ({})", record.number, record.html_url);
}
Ok(())
}

View file

@ -7,6 +7,7 @@ use tracing::info;
use fabro_workflow::run_lookup::runs_base;
use crate::args::{GlobalArgs, PrViewArgs};
use crate::shared::print_json_pretty;
use crate::user_config::load_user_settings_with_globals;
pub(super) async fn view_command(
@ -16,13 +17,14 @@ pub(super) async fn view_command(
) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
view_from(&base, args, github_app).await
view_from(&base, args, github_app, globals).await
}
async fn view_from(
base: &Path,
args: PrViewArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let (record, _run_dir) = super::load_pr_record(base, &args.run_id).await?;
@ -42,6 +44,11 @@ async fn view_from(
info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request");
if globals.json {
print_json_pretty(&detail)?;
return Ok(());
}
println!("#{} {}", detail.number, detail.title);
let state_display = if detail.draft { "draft" } else { &detail.state };
println!("State: {state_display}");

View file

@ -15,6 +15,7 @@ use fabro_workflow::operations::{ValidateInput, WorkflowInput, validate};
use crate::args::{GlobalArgs, PreflightArgs};
use crate::shared::github::build_github_app_credentials;
use crate::shared::print_json_pretty;
use crate::user_config::{load_user_settings_with_globals, user_layer_with_globals};
pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
@ -50,12 +51,14 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an
cwd,
custom_transforms: Vec::new(),
})?;
super::run::output::print_workflow_report(&validated, Some(&resolution.dot_path), styles);
if validated.has_errors() {
bail!("Validation failed");
if !globals.json {
super::run::output::print_workflow_report(&validated, Some(&resolution.dot_path), styles);
if validated.has_errors() {
bail!("Validation failed");
}
}
run_preflight(
let (report, preflight_ok) = run_preflight(
validated.graph(),
&settings,
args.model.as_deref(),
@ -66,8 +69,36 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an
styles,
github_app,
origin_url.as_deref(),
!globals.json,
)
.await
.await?;
if globals.json {
print_json_pretty(&serde_json::json!({
"workflow": {
"name": validated.graph().name,
"graph_path": resolution.dot_path,
"nodes": validated.graph().nodes.len(),
"edges": validated.graph().edges.len(),
"goal": validated.graph().goal(),
"diagnostics": validated.diagnostics(),
},
"checks": report,
}))?;
} else {
let term_width = console::Term::stderr().size().1;
print!("{}", report.render(styles, true, None, Some(term_width)));
}
if validated.has_errors() {
bail!("Validation failed");
}
if !preflight_ok {
std::process::exit(1);
}
Ok(())
}
fn resolve_model_provider(
@ -173,19 +204,23 @@ async fn run_preflight(
styles: &'static Styles,
github_app: Option<fabro_github::GitHubAppCredentials>,
origin_url: Option<&str>,
) -> anyhow::Result<()> {
show_progress: bool,
) -> anyhow::Result<(fabro_util::check_report::CheckReport, bool)> {
use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
};
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.expect("valid template")
.tick_strings(&["", "", "", "", "", "", "", "", "", "", ""]),
);
spinner.set_message("Running preflight checks...");
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
let spinner = show_progress.then(|| {
let spinner = indicatif::ProgressBar::new_spinner();
spinner.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.expect("valid template")
.tick_strings(&["", "", "", "", "", "", "", "", "", "", ""]),
);
spinner.set_message("Running preflight checks...");
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
spinner
});
let mut checks: Vec<CheckResult> = Vec::new();
@ -431,8 +466,6 @@ async fn run_preflight(
}
}
spinner.finish_and_clear();
let report = CheckReport {
title: "Run Preflight".into(),
sections: vec![CheckSection {
@ -440,13 +473,10 @@ async fn run_preflight(
checks,
}],
};
let term_width = console::Term::stderr().size().1;
print!("{}", report.render(styles, true, None, Some(term_width)));
if sandbox_ok && llm_ok {
Ok(())
} else {
std::process::exit(1);
if let Some(spinner) = spinner {
spinner.finish_and_clear();
}
let _ = styles;
Ok((report, sandbox_ok && llm_ok))
}

View file

@ -3,10 +3,11 @@ use fabro_model::Provider;
use fabro_util::terminal::Styles;
use tokio::task::spawn_blocking;
use crate::args::ProviderLoginArgs;
use crate::args::{GlobalArgs, ProviderLoginArgs};
use crate::shared::provider_auth;
pub(super) async fn login_command(args: ProviderLoginArgs) -> Result<()> {
pub(super) async fn login_command(args: ProviderLoginArgs, globals: &GlobalArgs) -> Result<()> {
globals.require_no_json()?;
let s = Styles::detect_stderr();
let arc_dir = dirs::home_dir()
.context("could not determine home directory")?

View file

@ -2,10 +2,10 @@ mod login;
use anyhow::Result;
use crate::args::{ProviderCommand, ProviderNamespace};
use crate::args::{GlobalArgs, ProviderCommand, ProviderNamespace};
pub(crate) async fn dispatch(ns: ProviderNamespace) -> Result<()> {
pub(crate) async fn dispatch(ns: ProviderNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
ProviderCommand::Login(args) => login::login_command(args).await,
ProviderCommand::Login(args) => login::login_command(args, globals).await,
}
}

View file

@ -1,7 +1,10 @@
use anyhow::{Context, Result, bail};
pub(crate) fn run_deinit() -> Result<()> {
use crate::args::GlobalArgs;
pub(crate) fn run_deinit(globals: &GlobalArgs) -> Result<Vec<String>> {
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();
let fabro_toml = repo_root.join("fabro.toml");
@ -15,29 +18,37 @@ pub(crate) fn run_deinit() -> Result<()> {
}
Err(e) => bail!("failed to remove {}: {e}", fabro_toml.display()),
}
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro.toml")
);
removed.push("fabro.toml".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro.toml")
);
}
let fabro_dir = repo_root.join("fabro");
if fabro_dir.exists() {
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
removed.push("fabro/".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro/")
);
}
}
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro/")
"\n{}",
console::Style::new()
.bold()
.apply_to("Project deinitialized.")
);
}
eprintln!(
"\n{}",
console::Style::new()
.bold()
.apply_to("Project deinitialized.")
);
Ok(())
Ok(removed)
}

View file

@ -3,6 +3,7 @@ use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use tokio::task::spawn_blocking;
use crate::args::GlobalArgs;
use crate::shared::github::build_github_app_credentials;
use crate::user_config::load_user_settings;
@ -21,8 +22,9 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
))
}
pub(crate) async fn run_init() -> Result<()> {
pub(crate) async fn run_init(globals: &GlobalArgs) -> Result<Vec<String>> {
let repo_root = git_repo_root()?;
let mut created = Vec::new();
let fabro_toml = repo_root.join("fabro.toml");
if fabro_toml.exists() {
@ -55,11 +57,14 @@ draft = true
",
)
.with_context(|| format!("failed to write {}", fabro_toml.display()))?;
created.push("fabro.toml".to_string());
let green = console::Style::new().green();
let bold = console::Style::new().bold();
let dim = console::Style::new().dim();
eprintln!(" {} {}", green.apply_to(""), dim.apply_to("fabro.toml"));
if !globals.json {
eprintln!(" {} {}", green.apply_to(""), dim.apply_to("fabro.toml"));
}
// Create hello workflow directory
let workflow_dir = repo_root.join("fabro/workflows/hello");
@ -84,11 +89,14 @@ draft = true
"#,
)
.with_context(|| format!("failed to write {}", dot_path.display()))?;
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.fabro")
);
created.push("fabro/workflows/hello/workflow.fabro".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.fabro")
);
}
// Create workflow.toml
let toml_path = workflow_dir.join("workflow.toml");
@ -97,24 +105,31 @@ draft = true
"version = 1\ngraph = \"workflow.fabro\"\n\n[sandbox]\nprovider = \"local\"\n",
)
.with_context(|| format!("failed to write {}", toml_path.display()))?;
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.toml")
);
created.push("fabro/workflows/hello/workflow.toml".to_string());
if !globals.json {
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("fabro/workflows/hello/workflow.toml")
);
}
eprintln!(
"\n{} Run a workflow with:\n\n {}",
bold.apply_to("Project initialized!"),
console::Style::new()
.cyan()
.bold()
.apply_to("fabro run hello")
);
if !globals.json {
eprintln!(
"\n{} Run a workflow with:\n\n {}",
bold.apply_to("Project initialized!"),
console::Style::new()
.cyan()
.bold()
.apply_to("fabro run hello")
);
}
check_github_app_installation().await;
if !globals.json {
check_github_app_installation().await;
}
Ok(())
Ok(created)
}
async fn check_github_app_installation() {

View file

@ -3,18 +3,28 @@ pub(crate) mod init;
use anyhow::Result;
use crate::args::{RepoCommand, RepoNamespace};
use crate::args::{GlobalArgs, RepoCommand, RepoNamespace};
use crate::shared::print_json_pretty;
pub(crate) async fn dispatch(ns: RepoNamespace) -> Result<()> {
pub(crate) async fn dispatch(ns: RepoNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
RepoCommand::Init { skill } => {
init::run_init().await?;
let created = init::run_init(globals).await?;
if skill {
let base = std::env::current_dir()?.join(".claude").join("skills");
super::skill::install_skill_to(&base)?;
}
if globals.json {
print_json_pretty(&serde_json::json!({ "created": created }))?;
}
Ok(())
}
RepoCommand::Deinit => {
let removed = deinit::run_deinit(globals)?;
if globals.json {
print_json_pretty(&serde_json::json!({ "removed": removed }))?;
}
Ok(())
}
RepoCommand::Deinit => deinit::run_deinit(),
}
}

View file

@ -38,6 +38,7 @@ pub(crate) async fn attach_run(
kill_on_detach: bool,
styles: &'static Styles,
engine_child: Option<std::process::Child>,
json_output: bool,
) -> Result<ExitCode> {
let run_record = RunRecord::load(run_dir).ok();
if let (Some(storage_dir), Some(run_id)) = (
@ -61,6 +62,7 @@ pub(crate) async fn attach_run(
kill_on_detach,
styles,
engine_child,
json_output,
)
.await;
}
@ -83,7 +85,7 @@ pub(crate) async fn attach_run(
}
}
attach_run_files(run_dir, kill_on_detach, styles, engine_child).await
attach_run_files(run_dir, kill_on_detach, styles, engine_child, json_output).await
}
async fn attach_run_store(
@ -94,6 +96,7 @@ async fn attach_run_store(
kill_on_detach: bool,
styles: &'static Styles,
engine_child: Option<std::process::Child>,
json_output: bool,
) -> Result<ExitCode> {
let runtime_state = RuntimeState::new(run_dir);
let runtime_interview_paths = InterviewPaths::from_runtime_state(&runtime_state);
@ -117,7 +120,7 @@ async fn attach_run_store(
}
for line in &existing_events {
progress_ui.handle_json_line(line);
emit_progress_line(&mut progress_ui, line, json_output)?;
}
let mut stream = run_store
@ -163,7 +166,7 @@ async fn attach_run_store(
match time::timeout(Duration::from_millis(100), stream.next()).await {
Ok(Some(Ok(event))) => {
let line = event_payload_line(&event)?;
progress_ui.handle_json_line(&line);
emit_progress_line(&mut progress_ui, &line, json_output)?;
saw_event = true;
}
Ok(Some(Err(err))) => return Err(err.into()),
@ -183,7 +186,7 @@ async fn attach_run_store(
serde_json::from_str::<fabro_interview::Question>(&request_data)
{
// Hide progress bars during interview
progress_ui.hide_bars();
hide_progress(&mut progress_ui, json_output);
// Prompt user via ConsoleInterviewer
let interviewer = ConsoleInterviewer::new(styles);
@ -191,7 +194,7 @@ async fn attach_run_store(
fabro_interview::Interviewer::ask(&interviewer, question).await;
// Show progress bars again before any return path.
progress_ui.show_bars();
show_progress(&mut progress_ui, json_output);
if answer_requires_reattach(&answer) {
if let Some(guard) = engine_guard.as_mut() {
@ -252,7 +255,7 @@ async fn attach_run_store(
}
}
progress_ui.finish();
finish_progress(&mut progress_ui, json_output);
Ok(determine_exit_code_with_store(run_store, run_dir).await)
}
@ -262,6 +265,7 @@ async fn attach_run_files(
kill_on_detach: bool,
styles: &'static Styles,
engine_child: Option<std::process::Child>,
json_output: bool,
) -> Result<ExitCode> {
let progress_path = run_dir.join("progress.jsonl");
let conclusion_path = run_dir.join("conclusion.json");
@ -293,7 +297,7 @@ async fn attach_run_files(
if let Some(record) = read_status_record(&status_path) {
if record.status.is_terminal() {
progress_ui.finish();
finish_progress(&mut progress_ui, json_output);
return Ok(determine_exit_code(&conclusion_path, Some(record)));
}
}
@ -301,7 +305,7 @@ async fn attach_run_files(
if let Some(guard) = engine_guard.as_mut() {
if let Some(child) = guard.inner() {
if matches!(child.try_wait(), Ok(Some(_))) {
progress_ui.finish();
finish_progress(&mut progress_ui, json_output);
return Ok(determine_exit_code(
&conclusion_path,
read_status_record(&status_path),
@ -312,7 +316,7 @@ async fn attach_run_files(
if let Some(pid) = read_launcher_pid(run_dir) {
if !process_alive(pid) && wait_count > 5 {
progress_ui.finish();
finish_progress(&mut progress_ui, json_output);
return Ok(determine_exit_code(
&conclusion_path,
read_status_record(&status_path),
@ -379,7 +383,7 @@ async fn attach_run_files(
}
let trimmed = line.trim();
if !trimmed.is_empty() {
progress_ui.handle_json_line(trimmed);
emit_progress_line(&mut progress_ui, trimmed, json_output)?;
}
}
@ -394,13 +398,13 @@ async fn attach_run_files(
if let Ok(question) =
serde_json::from_str::<fabro_interview::Question>(&request_data)
{
progress_ui.hide_bars();
hide_progress(&mut progress_ui, json_output);
let interviewer = ConsoleInterviewer::new(styles);
let answer =
fabro_interview::Interviewer::ask(&interviewer, question).await;
progress_ui.show_bars();
show_progress(&mut progress_ui, json_output);
if answer_requires_reattach(&answer) {
if let Some(guard) = engine_guard.as_mut() {
@ -433,12 +437,12 @@ async fn attach_run_files(
if let Some(child_alive) = child_alive_via_handle {
if !child_alive {
drain_remaining(&mut reader, &mut line, &mut progress_ui);
drain_remaining(&mut reader, &mut line, &mut progress_ui, json_output)?;
break;
}
} else {
if terminal_status.is_some() {
drain_remaining(&mut reader, &mut line, &mut progress_ui);
drain_remaining(&mut reader, &mut line, &mut progress_ui, json_output)?;
break;
}
@ -455,7 +459,7 @@ async fn attach_run_files(
}
};
if !engine_alive {
drain_remaining(&mut reader, &mut line, &mut progress_ui);
drain_remaining(&mut reader, &mut line, &mut progress_ui, json_output)?;
break;
}
}
@ -463,7 +467,7 @@ async fn attach_run_files(
sleep(Duration::from_millis(100)).await;
}
progress_ui.finish();
finish_progress(&mut progress_ui, json_output);
Ok(determine_exit_code(
&conclusion_path,
@ -475,7 +479,8 @@ fn drain_remaining(
reader: &mut BufReader<std::fs::File>,
line: &mut String,
progress_ui: &mut run_progress::ProgressUI,
) {
json_output: bool,
) -> Result<()> {
loop {
line.clear();
match reader.read_line(line) {
@ -483,11 +488,45 @@ fn drain_remaining(
Ok(_) => {
let trimmed = line.trim();
if !trimmed.is_empty() {
progress_ui.handle_json_line(trimmed);
emit_progress_line(progress_ui, trimmed, json_output)?;
}
}
}
}
Ok(())
}
fn emit_progress_line(
progress_ui: &mut run_progress::ProgressUI,
line: &str,
json_output: bool,
) -> Result<()> {
if json_output {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
writeln!(handle, "{line}")?;
} else {
progress_ui.handle_json_line(line);
}
Ok(())
}
fn finish_progress(progress_ui: &mut run_progress::ProgressUI, json_output: bool) {
if !json_output {
progress_ui.finish();
}
}
fn hide_progress(progress_ui: &mut run_progress::ProgressUI, json_output: bool) {
if !json_output {
progress_ui.hide_bars();
}
}
fn show_progress(progress_ui: &mut run_progress::ProgressUI, json_output: bool) {
if !json_output {
progress_ui.show_bars();
}
}
fn event_payload_line(event: &EventEnvelope) -> Result<String> {
@ -736,9 +775,16 @@ mod tests {
.unwrap();
let started = Instant::now();
let exit = attach_run(dir.path(), None, false, no_color_styles(), Some(child))
.await
.unwrap();
let exit = attach_run(
dir.path(),
None,
false,
no_color_styles(),
Some(child),
false,
)
.await
.unwrap();
assert_eq!(exit, ExitCode::from(0));
assert!(
@ -757,7 +803,7 @@ mod tests {
Some(StatusReason::LaunchFailed),
);
let exit = attach_run(dir.path(), None, false, no_color_styles(), None)
let exit = attach_run(dir.path(), None, false, no_color_styles(), None, false)
.await
.unwrap();

View file

@ -2,6 +2,7 @@ use anyhow::Result;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, RunArgs};
use crate::shared::print_json_pretty;
use crate::user_config::{self, user_layer_with_globals};
pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<()> {
@ -23,11 +24,24 @@ pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<(
let child = super::start::start_run(&run_dir, false)?;
if args.detach {
println!("{run_id}");
if globals.json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
} else {
println!("{run_id}");
}
} else {
let exit_code =
super::attach::attach_run(&run_dir, Some(&run_id), true, styles, Some(child)).await?;
super::output::print_run_summary(&run_dir, run_id, styles);
let exit_code = super::attach::attach_run(
&run_dir,
Some(&run_id),
true,
styles,
Some(child),
globals.json,
)
.await?;
if !globals.json {
super::output::print_run_summary(&run_dir, run_id, styles);
}
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}

View file

@ -10,7 +10,7 @@ use tokio::fs;
use tracing::{debug, info};
use crate::args::{CpArgs, GlobalArgs};
use crate::shared::split_run_path;
use crate::shared::{print_json_pretty, split_run_path};
use crate::user_config::load_user_settings_with_globals;
enum CopyDirection {
@ -39,15 +39,30 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()>
} => {
let sandbox = load_sandbox(&base, &run_prefix).await?;
if args.recursive {
download_recursive(&*sandbox, &remote_path, &local_path).await?;
let file_count = if args.recursive {
Some(download_recursive(&*sandbox, &remote_path, &local_path).await?)
} else {
debug!(path = %remote_path, "Downloading file from sandbox");
sandbox
.download_file_to_local(&remote_path, &local_path)
.await
.map_err(|err| anyhow::anyhow!("{err}"))?;
None
};
if globals.json {
let mut value = serde_json::json!({
"direction": "download",
"recursive": args.recursive,
"remote_path": remote_path,
"local_path": local_path,
});
if let Some(count) = file_count {
value["file_count"] = count.into();
}
print_json_pretty(&value)?;
}
info!(direction = "download", path = %remote_path, "Copy complete");
}
CopyDirection::Upload {
@ -57,14 +72,28 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()>
} => {
let sandbox = load_sandbox(&base, &run_prefix).await?;
if args.recursive {
upload_recursive(&*sandbox, &local_path, &remote_path).await?;
let file_count = if args.recursive {
Some(upload_recursive(&*sandbox, &local_path, &remote_path).await?)
} else {
debug!(path = %remote_path, "Uploading file to sandbox");
sandbox
.upload_file_from_local(&local_path, &remote_path)
.await
.map_err(|err| anyhow::anyhow!("{err}"))?;
None
};
if globals.json {
let mut value = serde_json::json!({
"direction": "upload",
"recursive": args.recursive,
"remote_path": remote_path,
"local_path": local_path,
});
if let Some(count) = file_count {
value["file_count"] = count.into();
}
print_json_pretty(&value)?;
}
info!(direction = "upload", path = %remote_path, "Copy complete");
}
@ -111,7 +140,7 @@ async fn download_recursive(
sandbox: &dyn Sandbox,
remote_path: &str,
local_path: &Path,
) -> Result<()> {
) -> Result<usize> {
let entries = sandbox
.list_directory(remote_path, Some(100))
.await
@ -137,14 +166,14 @@ async fn download_recursive(
file_count += 1;
}
debug!(count = file_count, "Recursive download complete");
Ok(())
Ok(file_count)
}
async fn upload_recursive(
sandbox: &dyn Sandbox,
local_path: &Path,
remote_path: &str,
) -> Result<()> {
) -> Result<usize> {
let mut file_count = 0usize;
let mut stack = vec![(local_path.to_path_buf(), remote_path.to_string())];
@ -171,7 +200,7 @@ async fn upload_recursive(
}
}
debug!(count = file_count, "Recursive upload complete");
Ok(())
Ok(file_count)
}
#[cfg(test)]

View file

@ -11,6 +11,7 @@ use fabro_workflow::sandbox_git::GIT_REMOTE;
use tracing::{debug, info};
use crate::args::{DiffArgs, GlobalArgs};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -24,6 +25,22 @@ pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> {
let patch = resolve_diff(&run.path, run_store.as_deref(), &args).await?;
if globals.json {
let mut value = serde_json::json!({
"run_id": run.run_id,
"node": args.node,
});
if args.shortstat {
value["shortstat"] = patch.trim_end().into();
} else if args.stat {
value["stat"] = patch.trim_end().into();
} else {
value["diff"] = patch.into();
}
print_json_pretty(&value)?;
return Ok(());
}
let is_tty = io::stdout().is_terminal();
let mut stdout = io::stdout().lock();
if is_tty {

View file

@ -9,6 +9,7 @@ use fabro_workflow::operations::{
use git2::Repository;
use crate::args::{ForkArgs, GlobalArgs};
use crate::shared::print_json_pretty;
use crate::store::{build_store, open_run_reader};
use crate::user_config::load_user_settings_with_globals;
@ -24,6 +25,10 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs)
let timeline = build_timeline_or_rebuild(&store, run_store.as_deref(), &run_id).await?;
if args.list {
if globals.json {
print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?;
return Ok(());
}
super::rewind::print_timeline(&timeline, styles);
return Ok(());
}
@ -45,15 +50,24 @@ pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs)
let run_id_string = run_id.to_string();
let new_run_id_string = new_run_id.to_string();
eprintln!(
"\nForked run {} -> {}",
&run_id_string[..8.min(run_id_string.len())],
&new_run_id_string[..8.min(new_run_id_string.len())]
);
eprintln!(
"To resume: fabro resume {}",
&new_run_id_string[..8.min(new_run_id_string.len())]
);
if globals.json {
let target = args.target.clone().unwrap_or_else(|| "latest".to_string());
print_json_pretty(&serde_json::json!({
"source_run_id": run_id_string,
"new_run_id": new_run_id_string,
"target": target,
}))?;
} else {
eprintln!(
"\nForked run {} -> {}",
&run_id_string[..8.min(run_id_string.len())],
&new_run_id_string[..8.min(new_run_id_string.len())]
);
eprintln!(
"To resume: fabro resume {}",
&new_run_id_string[..8.min(new_run_id_string.len())]
);
}
Ok(())
}

View file

@ -65,9 +65,10 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs)
let stdout = io::stdout();
let is_tty = stdout.is_terminal();
let mut out = stdout.lock();
let pretty = args.pretty && !globals.json;
for line in &filtered {
if args.pretty {
if pretty {
if let Some(formatted) = format_event_pretty(line, styles) {
writeln!(out, "{formatted}")?;
}
@ -82,7 +83,7 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs)
match follow_store_logs(
run_store.as_ref(),
if last_seq == 0 { 1 } else { last_seq + 1 },
args.pretty,
pretty,
styles,
is_tty,
)
@ -103,7 +104,7 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs)
&progress_path,
&run.path,
lines_seen,
args.pretty,
pretty,
styles,
is_tty,
)?;
@ -117,7 +118,7 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs)
&progress_path,
&run.path,
all_lines.len(),
args.pretty,
pretty,
styles,
is_tty,
)?;

View file

@ -4,6 +4,7 @@ use fabro_util::terminal::Styles;
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use crate::args::{GlobalArgs, RunCommands};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::{load_user_settings_with_globals, user_layer_with_globals};
@ -37,7 +38,11 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = user_layer_with_globals(globals)?;
let (run_id, _run_dir) = create::create_run(&args, cli, styles, true)?;
println!("{run_id}");
if globals.json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
} else {
println!("{run_id}");
}
Ok(())
}
RunCommands::Start { run } => {
@ -46,7 +51,11 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
let child = start::start_run(&run_info.path, false)?;
eprintln!("Started engine process (PID {})", child.id());
if globals.json {
print_json_pretty(&serde_json::json!({ "run_id": run_info.run_id }))?;
} else {
eprintln!("Started engine process (PID {})", child.id());
}
Ok(())
}
RunCommands::Attach { run } => {
@ -55,9 +64,15 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
let exit_code =
attach::attach_run(&run_info.path, Some(&run_info.run_id), false, styles, None)
.await?;
let exit_code = attach::attach_run(
&run_info.path,
Some(&run_info.run_id),
false,
styles,
None,
globals.json,
)
.await?;
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}

View file

@ -6,7 +6,7 @@ use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, PreviewArgs};
use crate::shared::validate_daytona_provider;
use crate::shared::{print_json_pretty, validate_daytona_provider};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -49,9 +49,13 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> {
.get_signed_preview_url(args.port, Some(args.ttl))
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
print!("{}", format_signed_output(&signed.url));
if globals.json {
print_json_pretty(&serde_json::json!({ "url": signed.url }))?;
} else {
print!("{}", format_signed_output(&signed.url));
}
if args.open {
if args.open && !globals.json {
std::process::Command::new("open")
.arg(&signed.url)
.spawn()
@ -62,7 +66,14 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> {
.get_preview_link(args.port)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
print!("{}", format_standard_output(&preview.url, &preview.token));
if globals.json {
print_json_pretty(&serde_json::json!({
"url": preview.url,
"token": preview.token,
}))?;
} else {
print!("{}", format_standard_output(&preview.url, &preview.token));
}
}
Ok(())

View file

@ -5,6 +5,7 @@ use fabro_workflow::records::{RunRecord, RunRecordExt};
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use crate::args::{GlobalArgs, ResumeArgs};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -37,11 +38,24 @@ pub(crate) async fn resume_command(
let child = super::start::start_run(&run_dir, true)?;
if args.detach {
println!("{run_id}");
if globals.json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
} else {
println!("{run_id}");
}
} else {
let exit_code =
super::attach::attach_run(&run_dir, Some(&run_id), true, styles, Some(child)).await?;
super::output::print_run_summary(&run_dir, run_id, styles);
let exit_code = super::attach::attach_run(
&run_dir,
Some(&run_id),
true,
styles,
Some(child),
globals.json,
)
.await?;
if !globals.json {
super::output::print_run_summary(&run_dir, run_id, styles);
}
if exit_code != std::process::ExitCode::SUCCESS {
std::process::exit(1);
}

View file

@ -14,12 +14,21 @@ use fabro_workflow::records::CheckpointExt;
use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflow::run_status::{self, RunStatus};
use git2::Repository;
use serde::Serialize;
use crate::args::{GlobalArgs, RewindArgs};
use crate::shared::color_if;
use crate::shared::{color_if, print_json_pretty};
use crate::store::{build_store, open_run_reader};
use crate::user_config::load_user_settings_with_globals;
#[derive(Serialize)]
pub(crate) struct TimelineEntryJson {
ordinal: usize,
node_name: String,
visit: usize,
run_commit_sha: Option<String>,
}
pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let cli_settings = load_user_settings_with_globals(globals)?;
@ -39,6 +48,10 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs
let timeline = build_timeline_or_rebuild(&store, run_store.as_deref(), &run_id).await?;
if args.list || args.target.is_none() {
if globals.json {
print_json_pretty(&timeline_entries_json(&timeline))?;
return Ok(());
}
print_timeline(&timeline, styles);
return Ok(());
}
@ -59,14 +72,34 @@ pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs
let run_id_string = run_id.to_string();
eprintln!(
"\nTo resume: fabro resume {}",
&run_id_string[..8.min(run_id_string.len())]
);
if globals.json {
print_json_pretty(&serde_json::json!({
"run_id": run_id_string,
"target": args.target.as_deref().unwrap(),
}))?;
} else {
eprintln!(
"\nTo resume: fabro resume {}",
&run_id_string[..8.min(run_id_string.len())]
);
}
Ok(())
}
pub(crate) fn timeline_entries_json(timeline: &RunTimeline) -> Vec<TimelineEntryJson> {
timeline
.entries
.iter()
.map(|entry| TimelineEntryJson {
ordinal: entry.ordinal,
node_name: entry.node_name.clone(),
visit: entry.visit,
run_commit_sha: entry.run_commit_sha.clone(),
})
.collect()
}
async fn reset_rewound_run_state(
git_store: &Store,
durable_store: &dyn fabro_store::Store,

View file

@ -6,11 +6,15 @@ use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::{GlobalArgs, SshArgs};
use crate::shared::validate_daytona_provider;
use crate::shared::{print_json_pretty, validate_daytona_provider};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> {
if globals.json && !args.print {
globals.require_no_json()?;
}
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
@ -50,7 +54,11 @@ pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> {
.map_err(|e| anyhow::anyhow!("{e}"))?;
if args.print {
print!("{}", format_output(&ssh_cmd));
if globals.json {
print_json_pretty(&serde_json::json!({ "command": ssh_cmd }))?;
} else {
print!("{}", format_output(&ssh_cmd));
}
} else {
exec_ssh(&ssh_cmd)?;
}

View file

@ -44,6 +44,7 @@ pub(crate) fn start_run(run_dir: &Path, resume: bool) -> Result<std::process::Ch
if resume {
cmd.arg("--resume");
}
cmd.env_remove("FABRO_JSON");
cmd.stdout(stdout_log)
.stderr(log_file)
.stdin(std::process::Stdio::null());

View file

@ -75,7 +75,7 @@ pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs)
None => Conclusion::load(&conclusion_path).ok(),
};
if args.json {
if globals.json {
let json_value = build_json_output(final_status, &run_info.run_id, conclusion.as_ref());
let mut out = std::io::stdout().lock();
serde_json::to_writer_pretty(&mut out, &json_value)?;

View file

@ -42,6 +42,11 @@ pub(crate) async fn list_command(
},
);
if globals.json {
println!("{}", serde_json::to_string_pretty(&filtered)?);
return Ok(());
}
if args.quiet {
for run in &filtered {
println!("{}", run.run_id);
@ -49,11 +54,6 @@ pub(crate) async fn list_command(
return Ok(());
}
if args.json {
println!("{}", serde_json::to_string_pretty(&filtered)?);
return Ok(());
}
if filtered.is_empty() {
if args.all {
eprintln!("No runs found.");

View file

@ -11,6 +11,7 @@ use fabro_workflow::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflow::run_status::{RunStatus, RunStatusRecord, write_run_status};
use crate::args::{GlobalArgs, RunsRemoveArgs};
use crate::shared::print_json_pretty;
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -20,17 +21,30 @@ pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs)
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
remove_from(args, store.as_ref(), &base).await
remove_from(args, store.as_ref(), &base, globals).await
}
async fn remove_from(args: &RunsRemoveArgs, store: &dyn Store, base: &Path) -> Result<()> {
async fn remove_from(
args: &RunsRemoveArgs,
store: &dyn Store,
base: &Path,
globals: &GlobalArgs,
) -> Result<()> {
let mut had_errors = false;
let mut removed = Vec::new();
let mut errors = Vec::new();
for identifier in &args.runs {
let run = match resolve_run_combined(store, base, identifier).await {
Ok(run) => run,
Err(err) => {
eprintln!("error: {identifier}: {err}");
if !globals.json {
eprintln!("error: {identifier}: {err}");
}
errors.push(serde_json::json!({
"identifier": identifier,
"error": err.to_string(),
}));
had_errors = true;
continue;
}
@ -38,11 +52,18 @@ async fn remove_from(args: &RunsRemoveArgs, store: &dyn Store, base: &Path) -> R
if run.status.is_active() && !args.force {
let run_id = run.run_id.to_string();
eprintln!(
let error = format!(
"cannot remove active run {} (status: {}, use -f to force)",
short_run_id(&run_id),
run.status
);
if !globals.json {
eprintln!("{error}");
}
errors.push(serde_json::json!({
"identifier": identifier,
"error": error,
}));
had_errors = true;
continue;
}
@ -77,14 +98,46 @@ async fn remove_from(args: &RunsRemoveArgs, store: &dyn Store, base: &Path) -> R
}
}
std::fs::remove_dir_all(&run.path)
.with_context(|| format!("failed to delete {}", run.path.display()))?;
store
let run_id = run.run_id.to_string();
if let Err(err) = std::fs::remove_dir_all(&run.path)
.with_context(|| format!("failed to delete {}", run.path.display()))
{
if !globals.json {
eprintln!("error: {identifier}: {err}");
}
errors.push(serde_json::json!({
"identifier": identifier,
"error": err.to_string(),
}));
had_errors = true;
continue;
}
if let Err(err) = store
.delete_run(&run.run_id)
.await
.with_context(|| format!("failed to delete store state for {}", run.run_id))?;
let run_id = run.run_id.to_string();
eprintln!("{}", short_run_id(&run_id));
.with_context(|| format!("failed to delete store state for {}", run.run_id))
{
if !globals.json {
eprintln!("error: {identifier}: {err}");
}
errors.push(serde_json::json!({
"identifier": identifier,
"error": err.to_string(),
}));
had_errors = true;
continue;
}
removed.push(run_id.clone());
if !globals.json {
eprintln!("{}", short_run_id(&run_id));
}
}
if globals.json {
print_json_pretty(&serde_json::json!({
"removed": removed,
"errors": errors,
}))?;
}
if had_errors {

View file

@ -1,13 +1,21 @@
use anyhow::{Result, bail};
use crate::args::SecretGetArgs;
use crate::args::{GlobalArgs, SecretGetArgs};
use crate::shared::print_json_pretty;
use fabro_config::dotenv;
pub(super) fn get_command(args: &SecretGetArgs) -> Result<()> {
pub(super) fn get_command(args: &SecretGetArgs, globals: &GlobalArgs) -> Result<()> {
let path = dotenv::env_file_path()?;
match dotenv::get_env_value(&path, &args.key)? {
Some(value) => {
println!("{value}");
if globals.json {
print_json_pretty(&serde_json::json!({
"key": args.key,
"value": value,
}))?;
} else {
println!("{value}");
}
Ok(())
}
None => bail!("secret not found: {}", args.key),

View file

@ -1,16 +1,36 @@
use anyhow::{Result, bail};
use crate::args::SecretListArgs;
use crate::args::{GlobalArgs, SecretListArgs};
use crate::shared::print_json_pretty;
use fabro_config::dotenv;
pub(super) fn list_command(args: &SecretListArgs) -> Result<()> {
pub(super) fn list_command(args: &SecretListArgs, globals: &GlobalArgs) -> Result<()> {
let path = dotenv::env_file_path()?;
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if globals.json {
print_json_pretty(&Vec::<serde_json::Value>::new())?;
}
return Ok(());
}
Err(e) => bail!("failed to read {}: {e}", path.display()),
};
let pairs = dotenv::parse_env(&contents);
if globals.json {
let values = pairs
.into_iter()
.map(|(key, value)| {
if args.show_values {
serde_json::json!({ "key": key, "value": value })
} else {
serde_json::json!({ "key": key })
}
})
.collect::<Vec<_>>();
print_json_pretty(&values)?;
return Ok(());
}
for (key, value) in pairs {
if args.show_values {
println!("{key}={value}");

View file

@ -5,13 +5,13 @@ mod set;
use anyhow::Result;
use crate::args::{SecretCommand, SecretNamespace};
use crate::args::{GlobalArgs, SecretCommand, SecretNamespace};
pub(crate) fn dispatch(ns: SecretNamespace) -> Result<()> {
pub(crate) fn dispatch(ns: SecretNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
SecretCommand::Get(args) => get::get_command(&args),
SecretCommand::List(args) => list::list_command(&args),
SecretCommand::Rm(args) => rm::rm_command(&args),
SecretCommand::Set(args) => set::set_command(&args),
SecretCommand::Get(args) => get::get_command(&args, globals),
SecretCommand::List(args) => list::list_command(&args, globals),
SecretCommand::Rm(args) => rm::rm_command(&args, globals),
SecretCommand::Set(args) => set::set_command(&args, globals),
}
}

View file

@ -1,9 +1,10 @@
use anyhow::{Result, bail};
use crate::args::SecretRmArgs;
use crate::args::{GlobalArgs, SecretRmArgs};
use crate::shared::print_json_pretty;
use fabro_config::dotenv;
pub(super) fn rm_command(args: &SecretRmArgs) -> Result<()> {
pub(super) fn rm_command(args: &SecretRmArgs, globals: &GlobalArgs) -> Result<()> {
let path = dotenv::env_file_path()?;
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
@ -16,7 +17,11 @@ pub(super) fn rm_command(args: &SecretRmArgs) -> Result<()> {
match updated {
Some(new_contents) => {
dotenv::write_env_file(&path, &new_contents)?;
eprintln!("Removed {}", args.key);
if globals.json {
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
} else {
eprintln!("Removed {}", args.key);
}
Ok(())
}
None => bail!("secret not found: {}", args.key),

View file

@ -1,13 +1,18 @@
use anyhow::Result;
use crate::args::SecretSetArgs;
use crate::args::{GlobalArgs, SecretSetArgs};
use crate::shared::print_json_pretty;
use fabro_config::dotenv;
pub(super) fn set_command(args: &SecretSetArgs) -> Result<()> {
pub(super) fn set_command(args: &SecretSetArgs, globals: &GlobalArgs) -> Result<()> {
let path = dotenv::env_file_path()?;
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let merged = dotenv::merge_env(&existing, &[(&args.key, &args.value)]);
dotenv::write_env_file(&path, &merged)?;
eprintln!("Set {}", args.key);
if globals.json {
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
} else {
eprintln!("Set {}", args.key);
}
Ok(())
}

View file

@ -3,7 +3,8 @@ use std::path::Path;
use anyhow::{Result, bail};
use tracing::{debug, info};
use crate::args::{SkillDir, SkillInstallArgs, SkillScope};
use crate::args::{GlobalArgs, SkillDir, SkillInstallArgs, SkillScope};
use crate::shared::{absolute_or_current, print_json_pretty};
const SKILL_MD: &str = include_str!("../../../../../../skills/fabro-create-workflow/SKILL.md");
const REF_DOT_LANGUAGE: &str =
@ -37,10 +38,14 @@ pub(crate) fn install_skill_to(base_dir: &Path) -> Result<()> {
Ok(())
}
pub(super) fn run_skill_install(args: &SkillInstallArgs) -> Result<()> {
pub(super) fn run_skill_install(args: &SkillInstallArgs, globals: &GlobalArgs) -> Result<()> {
let base_dir = resolve_base_dir(&args.scope, &args.dir)?;
let skill_dir = base_dir.join("fabro-create-workflow");
if globals.json && skill_dir.exists() && !args.force {
globals.require_no_json()?;
}
if skill_dir.exists() && !args.force {
let confirm = dialoguer::Confirm::new()
.with_prompt(format!(
@ -55,7 +60,17 @@ pub(super) fn run_skill_install(args: &SkillInstallArgs) -> Result<()> {
}
}
install_skill_to(&base_dir)
install_skill_to(&base_dir)?;
if globals.json {
print_json_pretty(&serde_json::json!({
"skill": "fabro-create-workflow",
"path": absolute_or_current(&skill_dir),
"files": SKILL_FILES.iter().map(|(rel_path, _)| (*rel_path).to_string()).collect::<Vec<_>>(),
}))?;
}
Ok(())
}
fn resolve_base_dir(scope: &SkillScope, dir: &SkillDir) -> Result<std::path::PathBuf> {

View file

@ -2,12 +2,12 @@ mod install;
use anyhow::Result;
use crate::args::{SkillCommand, SkillNamespace};
use crate::args::{GlobalArgs, SkillCommand, SkillNamespace};
pub(crate) use install::install_skill_to;
pub(crate) fn dispatch(ns: SkillNamespace) -> Result<()> {
pub(crate) fn dispatch(ns: SkillNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
SkillCommand::Install(args) => install::run_skill_install(&args),
SkillCommand::Install(args) => install::run_skill_install(&args, globals),
}
}

View file

@ -10,6 +10,7 @@ use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::args::{GlobalArgs, StoreDumpArgs};
use crate::shared::{absolute_or_current, print_json_pretty};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
@ -28,11 +29,19 @@ pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) ->
})?;
let file_count = export_run(run_store.as_ref(), &args.output).await?;
println!(
"Exported {file_count} files for run {} to {}",
run.run_id,
args.output.display()
);
if globals.json {
print_json_pretty(&serde_json::json!({
"run_id": run.run_id,
"output_dir": absolute_or_current(&args.output),
"file_count": file_count,
}))?;
} else {
println!(
"Exported {file_count} files for run {} to {}",
run.run_id,
args.output.display()
);
}
Ok(())
}

View file

@ -5,15 +5,46 @@ use chrono::{DateTime, Utc};
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_config::FabroSettingsExt;
use serde::Serialize;
use fabro_workflow::run_lookup::{logs_base, runs_base, scan_runs_combined};
use fabro_workflow::run_status::RunStatus;
use crate::args::{DfArgs, GlobalArgs};
use crate::shared::format_size;
use crate::shared::{format_size, print_json_pretty};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
#[derive(Serialize)]
struct SummaryRow {
r#type: String,
count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
active: Option<u64>,
size_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
reclaimable_bytes: Option<u64>,
}
#[derive(Serialize)]
struct RunSizeRow {
run_id: String,
workflow_name: String,
status: RunStatus,
start_time: String,
size_bytes: u64,
reclaimable: bool,
}
#[derive(Serialize)]
struct DfOutput {
summary: Vec<SummaryRow>,
total_size_bytes: u64,
total_reclaimable_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
runs: Option<Vec<RunSizeRow>>,
}
pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let data_dir = cli_settings.storage_dir();
@ -26,6 +57,7 @@ pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()
&data_dir,
&runs_base_dir,
&logs_base_dir,
globals,
)
.await
}
@ -37,11 +69,13 @@ async fn df_from(
data_dir: &Path,
runs_base: &Path,
logs_base: &Path,
globals: &GlobalArgs,
) -> Result<()> {
struct RunSizeInfo {
run_id: String,
workflow_name: String,
status: RunStatus,
start_time: String,
start_time_dt: Option<DateTime<Utc>>,
size: u64,
}
@ -65,6 +99,7 @@ async fn df_from(
run_id: run.run_id.to_string(),
workflow_name: run.workflow_name.clone(),
status: run.status,
start_time: run.start_time.clone(),
start_time_dt: run.start_time_dt,
size,
});
@ -122,6 +157,52 @@ async fn df_from(
};
let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 };
if globals.json {
let summary = vec![
SummaryRow {
r#type: "runs".to_string(),
count: runs.len().try_into().unwrap(),
active: Some(active_count),
size_bytes: total_run_size,
reclaimable_bytes: Some(reclaimable_run_size),
},
SummaryRow {
r#type: "logs".to_string(),
count: log_count,
active: None,
size_bytes: total_log_size,
reclaimable_bytes: Some(total_log_size),
},
SummaryRow {
r#type: "databases".to_string(),
count: db_count,
active: None,
size_bytes: total_db_size,
reclaimable_bytes: Some(0),
},
];
let runs = args.verbose.then(|| {
run_details
.iter()
.map(|detail| RunSizeRow {
run_id: detail.run_id.clone(),
workflow_name: detail.workflow_name.clone(),
status: detail.status,
start_time: detail.start_time.clone(),
size_bytes: detail.size,
reclaimable: !detail.status.is_active(),
})
.collect::<Vec<_>>()
});
print_json_pretty(&DfOutput {
summary,
total_size_bytes: total_run_size + total_log_size + total_db_size,
total_reclaimable_bytes: reclaimable_run_size + total_log_size,
runs,
})?;
return Ok(());
}
let use_color = console::colors_enabled();
let color_choice = if use_color {
cli_table::ColorChoice::Auto

View file

@ -4,20 +4,29 @@ use anyhow::{Context, Result, bail};
use chrono::Utc;
use fabro_config::FabroSettingsExt;
use fabro_store::Store;
use serde::Serialize;
use tracing::{debug, info};
use fabro_workflow::run_lookup::{StatusFilter, filter_runs, runs_base, scan_runs_combined};
use crate::args::{GlobalArgs, RunsPruneArgs};
use crate::shared::format_size;
use crate::shared::{format_size, print_json_pretty};
use crate::store;
use crate::user_config::load_user_settings_with_globals;
#[derive(Serialize)]
struct PruneRunRow {
run_id: String,
dir_name: String,
workflow_name: String,
size_bytes: u64,
}
pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_user_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
prune_from(args, store.as_ref(), &base).await
prune_from(args, store.as_ref(), &base, globals).await
}
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
@ -36,7 +45,12 @@ pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
}
}
async fn prune_from(args: &RunsPruneArgs, store: &dyn Store, base: &Path) -> Result<()> {
async fn prune_from(
args: &RunsPruneArgs,
store: &dyn Store,
base: &Path,
globals: &GlobalArgs,
) -> Result<()> {
let runs = scan_runs_combined(store, base).await?;
let label_filters = parse_label_filters(&args.filter.label);
let mut filtered = filter_runs(
@ -70,11 +84,37 @@ async fn prune_from(args: &RunsPruneArgs, store: &dyn Store, base: &Path) -> Res
filtered.retain(|run| !run.status.is_active());
if filtered.is_empty() {
eprintln!("No matching runs to prune.");
if globals.json {
if args.yes {
print_json_pretty(&serde_json::json!({
"dry_run": false,
"deleted_count": 0,
"freed_bytes": 0,
}))?;
} else {
print_json_pretty(&serde_json::json!({
"dry_run": true,
"runs": Vec::<PruneRunRow>::new(),
"total_count": 0,
"total_size_bytes": 0,
}))?;
}
} else {
eprintln!("No matching runs to prune.");
}
return Ok(());
}
let total_bytes: u64 = filtered.iter().map(|run| dir_size(&run.path)).sum();
let rows: Vec<PruneRunRow> = filtered
.iter()
.map(|run| PruneRunRow {
run_id: run.run_id.to_string(),
dir_name: run.dir_name.clone(),
workflow_name: run.workflow_name.clone(),
size_bytes: dir_size(&run.path),
})
.collect();
let total_bytes: u64 = rows.iter().map(|row| row.size_bytes).sum();
info!(count = filtered.len(), bytes = total_bytes, "pruning runs");
if args.yes {
@ -86,11 +126,29 @@ async fn prune_from(args: &RunsPruneArgs, store: &dyn Store, base: &Path) -> Res
.await
.with_context(|| format!("failed to delete store state for {}", run.run_id))?;
}
eprintln!(
"{} run(s) deleted ({} freed).",
filtered.len(),
format_size(total_bytes)
);
if globals.json {
print_json_pretty(&serde_json::json!({
"dry_run": false,
"deleted_count": filtered.len(),
"freed_bytes": total_bytes,
}))?;
} else {
eprintln!(
"{} run(s) deleted ({} freed).",
filtered.len(),
format_size(total_bytes)
);
}
return Ok(());
}
if globals.json {
print_json_pretty(&serde_json::json!({
"dry_run": true,
"runs": rows,
"total_count": filtered.len(),
"total_size_bytes": total_bytes,
}))?;
return Ok(());
}

View file

@ -10,7 +10,8 @@ use tracing::debug;
use tokio::process::Command as TokioCommand;
use tokio::task::JoinHandle;
use crate::args::UpgradeArgs;
use crate::args::{GlobalArgs, UpgradeArgs};
use crate::shared::print_json_pretty;
// ── Download backend abstraction ───────────────────────────────────────────
@ -223,7 +224,7 @@ impl UpgradeCheckState {
// ── Main upgrade command ───────────────────────────────────────────────────
pub(crate) async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
pub(crate) async fn run_upgrade(args: UpgradeArgs, globals: &GlobalArgs) -> Result<()> {
let backend = select_backend().await;
let current =
@ -263,16 +264,31 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
}
}
std::cmp::Ordering::Equal if !args.force => {
eprintln!("Already on version {current}");
if globals.json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": current.to_string(),
}))?;
} else {
eprintln!("Already on version {current}");
}
return Ok(());
}
_ => {}
}
if args.dry_run {
eprintln!("Would upgrade fabro from {current} to {target}");
eprintln!(" tag: {tag}");
eprintln!(" target: {}", detect_target()?);
if globals.json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": target.to_string(),
"dry_run": true,
}))?;
} else {
eprintln!("Would upgrade fabro from {current} to {target}");
eprintln!(" tag: {tag}");
eprintln!(" target: {}", detect_target()?);
}
return Ok(());
}
@ -337,7 +353,14 @@ pub(crate) async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
let _ = fs::set_permissions(&current_exe, fs::Permissions::from_mode(0o755));
}
eprintln!("Upgraded fabro to {target}");
if globals.json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": target.to_string(),
}))?;
} else {
eprintln!("Upgraded fabro to {target}");
}
Ok(())
}

View file

@ -5,10 +5,14 @@ use fabro_util::terminal::Styles;
use fabro_validate::Severity;
use fabro_workflow::operations::{ValidateInput, WorkflowInput, validate};
use crate::args::ValidateArgs;
use crate::shared::{print_diagnostics, relative_path};
use crate::args::{GlobalArgs, ValidateArgs};
use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
pub(crate) fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> {
pub(crate) fn run(
args: &ValidateArgs,
styles: &Styles,
globals: &GlobalArgs,
) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let settings = ConfigLayer::for_workflow(&args.workflow, &cwd)?
.combine(ConfigLayer::user()?)
@ -23,6 +27,21 @@ pub(crate) fn run(args: &ValidateArgs, styles: &Styles) -> anyhow::Result<()> {
let graph = validated.graph();
let diagnostics = validated.diagnostics();
if globals.json {
print_json_pretty(&serde_json::json!({
"workflow_name": graph.name,
"nodes": graph.nodes.len(),
"edges": graph.edges.len(),
"valid": !diagnostics.iter().any(|d| d.severity == Severity::Error),
"diagnostics": diagnostics,
}))?;
if diagnostics.iter().any(|d| d.severity == Severity::Error) {
bail!("Validation failed");
}
return Ok(());
}
eprintln!(
"{} ({} nodes, {} edges)",
styles.bold.apply_to(format!("Workflow: {}", graph.name)),

View file

@ -4,10 +4,10 @@ use anyhow::{Context, Result, bail};
use fabro_config::project::{discover_project_config, resolve_fabro_root};
use crate::args::WorkflowCreateArgs;
use crate::shared::relative_path;
use crate::args::{GlobalArgs, WorkflowCreateArgs};
use crate::shared::{print_json_pretty, relative_path};
pub(super) fn create_command(args: &WorkflowCreateArgs) -> Result<()> {
pub(super) fn create_command(args: &WorkflowCreateArgs, globals: &GlobalArgs) -> Result<()> {
let cwd = std::env::current_dir()?;
let Some((config_path, config)) = discover_project_config(&cwd)? else {
@ -18,7 +18,15 @@ pub(super) fn create_command(args: &WorkflowCreateArgs) -> Result<()> {
};
let fabro_root = resolve_fabro_root(&config_path, &config);
write_workflow_scaffold(args, &fabro_root)?;
let created = write_workflow_scaffold(args, &fabro_root)?;
if globals.json {
print_json_pretty(&serde_json::json!({
"name": args.name,
"created": created,
}))?;
return Ok(());
}
let workflows_dir = fabro_root.join("workflows").join(&args.name);
let green = console::Style::new().green();
@ -55,7 +63,7 @@ pub(super) fn create_command(args: &WorkflowCreateArgs) -> Result<()> {
Ok(())
}
fn write_workflow_scaffold(args: &WorkflowCreateArgs, fabro_root: &Path) -> Result<()> {
fn write_workflow_scaffold(args: &WorkflowCreateArgs, fabro_root: &Path) -> Result<Vec<String>> {
let workflows_dir = fabro_root.join("workflows").join(&args.name);
if workflows_dir.exists() {
@ -95,7 +103,10 @@ fn write_workflow_scaffold(args: &WorkflowCreateArgs, fabro_root: &Path) -> Resu
std::fs::write(&toml_path, "version = 1\n")
.with_context(|| format!("failed to write {}", toml_path.display()))?;
Ok(())
Ok(vec![
format!("fabro/workflows/{}/workflow.fabro", args.name),
format!("fabro/workflows/{}/workflow.toml", args.name),
])
}
fn to_pascal_case(s: &str) -> String {

View file

@ -6,12 +6,12 @@ use fabro_config::project::{
resolve_fabro_root,
};
use crate::args::WorkflowListArgs;
use crate::shared::relative_path;
use crate::args::{GlobalArgs, WorkflowListArgs};
use crate::shared::{print_json_pretty, relative_path};
const GOAL_MAX_LEN: usize = 60;
pub(super) fn list_command(_args: &WorkflowListArgs) -> Result<()> {
pub(super) fn list_command(_args: &WorkflowListArgs, globals: &GlobalArgs) -> Result<()> {
let styles = Styles::detect_stderr();
let cwd = std::env::current_dir()?;
@ -28,6 +28,11 @@ pub(super) fn list_command(_args: &WorkflowListArgs) -> Result<()> {
let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref());
if globals.json {
print_json_pretty(&workflows)?;
return Ok(());
}
let project: Vec<_> = workflows
.iter()
.filter(|w| w.source == WorkflowSource::Project)

View file

@ -3,11 +3,11 @@ mod list;
use anyhow::Result;
use crate::args::{WorkflowCommand, WorkflowNamespace};
use crate::args::{GlobalArgs, WorkflowCommand, WorkflowNamespace};
pub(crate) fn dispatch(ns: WorkflowNamespace) -> Result<()> {
pub(crate) fn dispatch(ns: WorkflowNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
WorkflowCommand::List(args) => list::list_command(&args),
WorkflowCommand::Create(args) => create::create_command(&args),
WorkflowCommand::List(args) => list::list_command(&args, globals),
WorkflowCommand::Create(args) => create::create_command(&args, globals),
}
}

View file

@ -173,14 +173,14 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Preflight(args) => commands::preflight::execute(args, &globals).await?,
Commands::Validate(args) => {
let styles = Styles::detect_stderr();
commands::validate::run(&args, &styles)?;
commands::validate::run(&args, &styles, &globals)?;
}
Commands::Graph(args) => {
let styles = Styles::detect_stderr();
commands::graph::run(&args, &styles)?;
commands::graph::run(&args, &styles, &globals)?;
}
Commands::Parse(args) => {
commands::parse::run(&args)?;
commands::parse::run(&args, &globals)?;
}
Commands::Asset(ns) => commands::asset::dispatch(ns, &globals)?,
Commands::Store(ns) => commands::store::dispatch(ns, &globals).await?,
@ -196,31 +196,44 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Doctor { verbose, dry_run } => {
let cli_settings = user_config::load_user_settings()?;
let verbose = verbose || cli_settings.verbose_enabled();
let exit_code = commands::doctor::run_doctor(verbose, !dry_run).await;
let exit_code = commands::doctor::run_doctor(verbose, !dry_run, &globals).await?;
std::process::exit(exit_code);
}
Commands::Discord => {
open::that("https://fabro.sh/discord")?;
if globals.json {
crate::shared::print_json_pretty(&serde_json::json!({
"url": "https://fabro.sh/discord",
}))?;
} else {
open::that("https://fabro.sh/discord")?;
}
}
Commands::Docs => {
open::that("https://docs.fabro.sh/")?;
if globals.json {
crate::shared::print_json_pretty(&serde_json::json!({
"url": "https://docs.fabro.sh/",
}))?;
} else {
open::that("https://docs.fabro.sh/")?;
}
}
Commands::Repo(ns) => commands::repo::dispatch(ns).await?,
Commands::Repo(ns) => commands::repo::dispatch(ns, &globals).await?,
Commands::Install { web_url } => {
commands::install::run_install(&web_url).await?;
commands::install::run_install(&web_url, &globals).await?;
}
Commands::Pr(ns) => commands::pr::dispatch(ns, &globals).await?,
Commands::Secret(ns) => commands::secret::dispatch(ns)?,
Commands::Secret(ns) => commands::secret::dispatch(ns, &globals)?,
Commands::Settings(args) => commands::config::execute(&args, &globals)?,
Commands::Workflow(ns) => commands::workflow::dispatch(ns)?,
Commands::Skill(ns) => commands::skill::dispatch(ns)?,
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals)?,
Commands::Skill(ns) => commands::skill::dispatch(ns, &globals)?,
Commands::Upgrade(args) => {
commands::upgrade::run_upgrade(args).await?;
commands::upgrade::run_upgrade(args, &globals).await?;
}
Commands::Provider(ns) => commands::provider::dispatch(ns).await?,
Commands::Provider(ns) => commands::provider::dispatch(ns, &globals).await?,
Commands::Sandbox { command } => commands::sandbox::dispatch(command, &globals).await?,
Commands::System(ns) => commands::system::dispatch(ns, &globals).await?,
Commands::Completion(args) => {
globals.require_no_json()?;
let mut cmd = Cli::command();
let shell = args.shell;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {

View file

@ -1,16 +1,29 @@
use std::path::Path;
use std::time::Duration;
use std::{io::Write, path::PathBuf};
use anyhow::{Result, bail};
use cli_table::Color;
use fabro_util::terminal::Styles;
use fabro_validate::{Diagnostic, Severity};
use serde::Serialize;
pub(crate) fn read_workflow_file(path: &Path) -> anyhow::Result<String> {
std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read {}: {e}", path.display()))
}
pub(crate) fn print_json_pretty<T>(value: &T) -> anyhow::Result<()>
where
T: Serialize + ?Sized,
{
let stdout = std::io::stdout();
let mut handle = stdout.lock();
serde_json::to_writer_pretty(&mut handle, value)?;
writeln!(handle)?;
Ok(())
}
pub(crate) fn print_diagnostics(diagnostics: &[Diagnostic], styles: &Styles) {
for d in diagnostics {
let location = match (&d.node_id, &d.edge) {
@ -69,6 +82,16 @@ pub(crate) fn tilde_path(path: &Path) -> String {
path.display().to_string()
}
pub(crate) fn absolute_or_current(path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(path)
} else {
path.to_path_buf()
}
}
pub(crate) fn color_if(use_color: bool, color: Color) -> Option<Color> {
if use_color { Some(color) } else { None }
}

View file

@ -19,6 +19,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -20,12 +20,13 @@ fn help() {
[DEST] Destination directory (defaults to current directory) [default: .]
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--node <NODE> Filter to assets from a specific node
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--retry <RETRY> Filter to assets from a specific retry attempt
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--tree Preserve {node_slug}/retry_{N}/ directory structure
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help

View file

@ -19,11 +19,11 @@ fn help() {
<RUN_ID> Run ID (or prefix)
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--node <NODE> Filter to assets from a specific node
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--retry <RETRY> Filter to assets from a specific retry attempt
--json Output as JSON
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]

View file

@ -21,6 +21,7 @@ fn help() {
<RUN> Run ID prefix or workflow name
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -17,6 +17,7 @@ fn help() {
<SHELL> Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -24,6 +24,7 @@ fn help() {
[WORKFLOW] Optional workflow name, .fabro path, or .toml run config to overlay
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -24,16 +24,17 @@ fn help() {
<WORKFLOW> Path to a .fabro workflow file or .toml task config
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--dry-run Execute with simulated LLM backend
--json Output as JSON [env: FABRO_JSON=]
--auto-approve Auto-approve all human gates
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--goal <GOAL> Override the workflow goal (exposed as $goal in prompts)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--goal-file <GOAL_FILE> Read the workflow goal from a file
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--model <MODEL> Override default LLM model
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--provider <PROVIDER> Override default LLM provider
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-v, --verbose Enable verbose output
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)

View file

@ -16,12 +16,13 @@ fn help() {
Usage: fabro __detached [OPTIONS] --run-dir <RUN_DIR> --launcher-path <LAUNCHER_PATH>
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--run-dir <RUN_DIR> Run directory
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--launcher-path <LAUNCHER_PATH> Launcher metadata path
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--resume Resume from checkpoint instead of fresh start
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help

View file

@ -19,12 +19,13 @@ fn help() {
<RUN> Run ID or prefix
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--node <NODE> Show diff for a specific node
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--stat Show diffstat instead of full patch (live diffs only)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--shortstat Show only files-changed/insertions/deletions summary (live diffs only)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help

View file

@ -14,6 +14,7 @@ fn help() {
Usage: fabro discord [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -14,6 +14,7 @@ fn help() {
Usage: fabro docs [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -15,8 +15,9 @@ fn help() {
Usage: fabro doctor [OPTIONS]
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
-v, --verbose Show detailed information for each check
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--dry-run Skip live service probes (LLM, sandbox, API, web, Brave Search)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -17,12 +17,13 @@ fn help() {
<PROMPT> Task prompt
Options:
--json Output as JSON [env: FABRO_JSON=]
--provider <PROVIDER> LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
--model <MODEL> Model name (defaults per provider)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--permissions <PERMISSIONS> Permission level for tool execution [possible values: read-only, read-write, full]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--auto-approve Skip interactive prompts; deny tools outside permission level
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--debug Print LLM request/response debug info to stderr
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--verbose Print full LLM request/response JSON to stderr

View file

@ -46,6 +46,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -25,8 +25,9 @@ fn help() {
[TARGET] Target checkpoint: node name, node@visit, or @ordinal (omit to fork from latest)
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--list Show the checkpoint timeline instead of forking
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-push Skip pushing new branches to the remote
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -20,21 +20,21 @@ fn help() {
Path to the .fabro workflow file, .toml task config, or project workflow name
Options:
--debug
Enable DEBUG-level logging (default is INFO)
[env: FABRO_DEBUG=]
--format <FORMAT>
Output format
[default: svg]
[possible values: svg, png]
--no-upgrade-check
Disable automatic upgrade check
--json
Output as JSON
[env: FABRO_NO_UPGRADE_CHECK=true]
[env: FABRO_JSON=]
--debug
Enable DEBUG-level logging (default is INFO)
[env: FABRO_DEBUG=]
-o, --output <OUTPUT>
Output file path (defaults to stdout)
@ -46,6 +46,11 @@ fn help() {
- lr: Left to right
- tb: Top to bottom
--no-upgrade-check
Disable automatic upgrade check
[env: FABRO_NO_UPGRADE_CHECK=true]
--quiet
Suppress non-essential output

View file

@ -24,6 +24,7 @@ fn help() {
<RUN> Run ID prefix or workflow name (most recent run)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -14,8 +14,9 @@ fn help() {
Usage: fabro install [OPTIONS]
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--web-url <WEB_URL> Base URL for the web UI (used for OAuth callback URLs) [default: http://localhost:5173]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
@ -24,3 +25,17 @@ fn help() {
----- stderr -----
");
}
#[test]
fn install_rejects_json() {
let context = test_context!();
let output = context
.command()
.args(["--json", "install"])
.output()
.expect("command should run");
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("--json is not supported for this command"));
}

View file

@ -0,0 +1,153 @@
use std::process::Command;
use fabro_test::test_context;
use serde_json::Value;
use super::support::{fixture, output_stderr, output_stdout, setup_completed_dry_run};
fn dot_is_available() -> bool {
Command::new("dot")
.arg("-V")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[test]
fn completion_rejects_json() {
let context = test_context!();
let output = context
.command()
.args(["--json", "completion", "zsh"])
.output()
.expect("command should run");
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("--json is not supported for this command"));
}
#[test]
fn settings_json_outputs_parseable_json() {
let context = test_context!();
let output = context
.settings()
.arg("--json")
.output()
.expect("command should run");
assert!(output.status.success());
let value: Value =
serde_json::from_slice(&output.stdout).expect("settings --json should parse");
assert!(value.is_object());
}
#[test]
fn ps_supports_global_flag_and_env_var() {
let context = test_context!();
setup_completed_dry_run(&context);
let global_output = context
.command()
.args(["--json", "ps", "-a"])
.output()
.expect("command should run");
assert!(global_output.status.success());
let global_runs: Value =
serde_json::from_slice(&global_output.stdout).expect("global --json should parse");
assert!(global_runs.as_array().is_some_and(|runs| !runs.is_empty()));
let env_output = context
.command()
.env("FABRO_JSON", "1")
.args(["ps", "-a"])
.output()
.expect("command should run");
assert!(env_output.status.success());
let env_runs: Value =
serde_json::from_slice(&env_output.stdout).expect("FABRO_JSON output should parse");
assert_eq!(global_runs, env_runs);
}
#[test]
fn logs_json_wins_over_pretty() {
let context = test_context!();
let run = setup_completed_dry_run(&context);
let output = context
.command()
.args(["--json", "logs", "--pretty", &run.run_id])
.output()
.expect("command should run");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let first_line = stdout.lines().find(|line| !line.is_empty()).unwrap();
let value: Value = serde_json::from_str(first_line).expect("logs output should remain JSONL");
assert!(value.get("event").is_some());
}
#[test]
fn graph_json_without_output_is_rejected() {
let context = test_context!();
let workflow = fixture("simple.fabro");
let output = context
.command()
.args(["--json", "graph", workflow.to_str().unwrap()])
.output()
.expect("command should run");
assert!(!output.status.success());
let stderr = output_stderr(&output);
assert!(stderr.contains("--json is not supported for this command"));
}
#[test]
fn graph_json_with_output_reports_file() {
if !dot_is_available() {
return;
}
let context = test_context!();
let output_path = context.temp_dir.join("graph.svg");
let workflow = fixture("simple.fabro");
let output = context
.command()
.args([
"--json",
"graph",
workflow.to_str().unwrap(),
"--output",
output_path.to_str().unwrap(),
])
.output()
.expect("command should run");
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
output_stdout(&output),
output_stderr(&output)
);
let value: Value = serde_json::from_slice(&output.stdout).expect("graph JSON should parse");
assert_eq!(value["format"], "svg");
assert_eq!(value["path"], output_path.to_string_lossy().to_string());
assert!(output_path.exists());
}
#[test]
fn secret_list_json_missing_env_is_empty_array() {
let context = test_context!();
let output = context
.command()
.args(["--json", "secret", "list"])
.output()
.expect("command should run");
assert!(output.status.success());
let value: Value = serde_json::from_slice(&output.stdout).expect("secret list should parse");
assert_eq!(value, Value::Array(vec![]));
}

View file

@ -17,17 +17,18 @@ fn help() {
[PROMPT] The prompt text (also accepts stdin)
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
-m, --model <MODEL> Model to use
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-s, --system <SYSTEM> System prompt
--no-stream Do not stream output
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
-u, --usage Show token usage
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-S, --schema <SCHEMA> JSON schema for structured output (inline JSON string)
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-o, --option <OPTION> key=value options (temperature, `max_tokens`, `top_p`)
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help
----- stderr -----
");

View file

@ -19,13 +19,14 @@ fn help() {
<RUN> Run ID prefix or workflow name (most recent run)
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-f, --follow Follow log output
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--since <SINCE> Logs since timestamp or relative (e.g. "42m", "2h", "2026-01-02T13:00:00Z")
-n, --tail <TAIL> Lines from end (default: all)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
-p, --pretty Formatted colored output with rendered assistant text
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help

View file

@ -16,6 +16,7 @@ mod fork;
mod graph;
mod inspect;
mod install;
mod json_global;
mod llm;
mod llm_prompt;
mod logs;

View file

@ -19,6 +19,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -14,10 +14,11 @@ fn help() {
Usage: fabro model list [OPTIONS]
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
-p, --provider <PROVIDER> Filter by provider
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-q, --query <QUERY> Search for models matching this string
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]

View file

@ -14,11 +14,12 @@ fn help() {
Usage: fabro model test [OPTIONS]
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
-p, --provider <PROVIDER> Filter by provider
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-m, --model <MODEL> Test a specific model
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--deep Run a multi-turn tool-use test (catches reasoning round-trip bugs)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]

View file

@ -17,6 +17,7 @@ fn help() {
<WORKFLOW> Path to the .fabro workflow file
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -22,6 +22,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -17,6 +17,7 @@ fn help() {
<RUN_ID> Run ID or prefix
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -19,8 +19,9 @@ fn help() {
<RUN_ID> Run ID or prefix
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--model <MODEL> LLM model for generating PR description
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]

View file

@ -15,6 +15,7 @@ fn help() {
Options:
--all Show all PRs (including closed/merged), not just open
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -17,8 +17,9 @@ fn help() {
<RUN_ID> Run ID or prefix
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--method <METHOD> Merge method: merge, squash, or rebase [default: squash]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]

View file

@ -19,6 +19,7 @@ fn help() {
<RUN_ID> Run ID or prefix
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -1,4 +1,5 @@
use fabro_test::{fabro_snapshot, test_context};
use serde_json::Value;
use super::support::fixture;
@ -19,16 +20,17 @@ fn help() {
<WORKFLOW> Path to a .fabro workflow file or .toml task config
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--goal <GOAL> Override the workflow goal (exposed as $goal in prompts)
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--goal-file <GOAL_FILE> Read the workflow goal from a file
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--model <MODEL> Override default LLM model
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--provider <PROVIDER> Override default LLM provider
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
-v, --verbose Enable verbose output
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help
----- stderr -----
");
@ -53,3 +55,28 @@ fn preflight_invalid_workflow_fails_with_validation_output() {
error: Validation failed
");
}
#[test]
fn preflight_invalid_workflow_json_emits_diagnostics() {
let context = test_context!();
let workflow = fixture("invalid.fabro");
let output = context
.command()
.args(["--json", "preflight", workflow.to_str().unwrap()])
.output()
.expect("command should run");
assert!(!output.status.success());
let value: Value =
serde_json::from_slice(&output.stdout).expect("preflight --json should parse");
assert_eq!(value["workflow"]["name"], "Invalid");
assert!(
value["workflow"]["diagnostics"]
.as_array()
.is_some_and(|diagnostics| !diagnostics.is_empty())
);
assert_eq!(value["checks"]["title"], "Run Preflight");
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("Validation failed"));
}

View file

@ -18,6 +18,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -14,8 +14,9 @@ fn help() {
Usage: fabro provider login [OPTIONS] --provider <PROVIDER>
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--provider <PROVIDER> LLM provider to authenticate with
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
@ -24,3 +25,17 @@ fn help() {
----- stderr -----
");
}
#[test]
fn provider_login_rejects_json() {
let context = test_context!();
let output = context
.command()
.args(["--json", "provider", "login", "--provider", "anthropic"])
.output()
.expect("command should run");
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("--json is not supported for this command"));
}

View file

@ -17,16 +17,16 @@ fn help() {
Options:
--before <BEFORE> Only include runs started before this date (YYYY-MM-DD prefix match)
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--workflow <WORKFLOW> Filter by workflow name (substring match)
--label <KEY=VALUE> Filter by label (KEY=VALUE, repeatable, AND semantics)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--orphans Include orphan directories (no run.json)
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--json Output as JSON
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-a, --all Show all runs, not just running (like docker ps -a)
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-q, --quiet Only display run IDs
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-h, --help Print help
----- stderr -----
");

View file

@ -26,6 +26,7 @@ fn help() {
help Print this message or the help of the given subcommand(s)
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
@ -116,6 +117,7 @@ fn test_repo_init_help_does_not_show_skill() {
Usage: fabro repo init [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -14,6 +14,7 @@ fn help() {
Usage: fabro repo deinit [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -16,6 +16,7 @@ fn help() {
Usage: fabro repo init [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -20,6 +20,7 @@ fn help() {
Options:
-d, --detach Run in the background and print the run ID
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -25,8 +25,9 @@ fn help() {
[TARGET] Target checkpoint: node name, node@visit, or @ordinal (omit with --list)
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--list Show the checkpoint timeline instead of rewinding
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-push Skip force-pushing rewound refs to the remote
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]

View file

@ -1,4 +1,5 @@
use fabro_test::{fabro_snapshot, test_context};
use serde_json::Value;
use super::support::{setup_completed_dry_run, setup_created_dry_run};
@ -19,8 +20,9 @@ fn help() {
<RUNS>... Run IDs or workflow names to remove
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
-f, --force Force removal of active runs
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
@ -140,3 +142,32 @@ fn rm_partial_failure_reports_which_identifiers_failed() {
"existing run should still be removed"
);
}
#[test]
fn rm_partial_failure_json_includes_removed_and_errors() {
let context = test_context!();
let run = setup_completed_dry_run(&context);
let output = context
.command()
.args(["--json", "rm", &run.run_id, "does-not-exist"])
.output()
.expect("command should run");
assert!(!output.status.success());
let value: Value = serde_json::from_slice(&output.stdout).expect("rm JSON should parse");
assert_eq!(
value["removed"],
Value::Array(vec![Value::String(run.run_id.clone())])
);
assert_eq!(value["errors"][0]["identifier"], "does-not-exist");
assert!(
value["errors"][0]["error"]
.as_str()
.is_some_and(|error| error.contains("does-not-exist"))
);
assert!(
!run.run_dir.exists(),
"existing run should still be removed"
);
}

View file

@ -22,16 +22,17 @@ fn help() {
<WORKFLOW> Path to a .fabro workflow file or .toml task config
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--dry-run Execute with simulated LLM backend
--json Output as JSON [env: FABRO_JSON=]
--auto-approve Auto-approve all human gates
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--goal <GOAL> Override the workflow goal (exposed as $goal in prompts)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--goal-file <GOAL_FILE> Read the workflow goal from a file
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--model <MODEL> Override default LLM model
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--provider <PROVIDER> Override default LLM provider
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
-v, --verbose Enable verbose output
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
--label <KEY=VALUE> Attach a label to this run (repeatable, format: KEY=VALUE)

View file

@ -20,8 +20,9 @@ fn help() {
<DST> Destination: <run-id>:<path> or local path
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
-r, --recursive Recurse into directories
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]

View file

@ -20,10 +20,11 @@ fn help() {
<PORT> Port number
Options:
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--json Output as JSON [env: FABRO_JSON=]
--signed Generate a signed URL (embeds auth token, no headers needed)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--ttl <TTL> Signed URL expiry in seconds (default 3600, requires --signed) [default: 3600]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--open Open URL in browser (implies --signed)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]

Some files were not shown because too many files have changed in this diff Show more