mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add global JSON output mode
This commit is contained in:
parent
c148a693b9
commit
e99ebfa3f2
125 changed files with 1762 additions and 426 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(¤t_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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(|| {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
153
lib/crates/fabro-cli/tests/it/cmd/json_global.rs
Normal file
153
lib/crates/fabro-cli/tests/it/cmd/json_global.rs
Normal 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![]));
|
||||
}
|
||||
|
|
@ -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 -----
|
||||
");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ mod fork;
|
|||
mod graph;
|
||||
mod inspect;
|
||||
mod install;
|
||||
mod json_global;
|
||||
mod llm;
|
||||
mod llm_prompt;
|
||||
mod logs;
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -----
|
||||
");
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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=]
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue