Merge remote-tracking branch 'origin/main'

This commit is contained in:
Bryan Helmkamp 2026-04-14 12:30:40 -04:00
commit 2047e490d8
61 changed files with 849 additions and 1230 deletions

View file

@ -3,6 +3,10 @@ use std::path::{Path, PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use fabro_agent::cli::AgentArgs;
use fabro_types::settings::cli::{
CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer, OutputFormat, OutputVerbosity,
};
use fabro_util::printer::Printer;
pub(crate) const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
@ -36,13 +40,46 @@ pub(crate) struct GlobalArgs {
pub verbose: bool,
}
impl GlobalArgs {
pub(crate) fn require_no_json(&self) -> anyhow::Result<()> {
anyhow::ensure!(!self.json, "--json is not supported for this command");
Ok(())
pub(crate) fn global_args_cli_layer(globals: &GlobalArgs) -> CliLayer {
let format = globals.json.then_some(OutputFormat::Json);
let verbosity = match (globals.quiet, globals.verbose) {
(true, _) => Some(OutputVerbosity::Quiet),
(_, true) => Some(OutputVerbosity::Verbose),
_ => None,
};
let output =
(format.is_some() || verbosity.is_some()).then_some(CliOutputLayer { format, verbosity });
let updates = globals
.no_upgrade_check
.then_some(CliUpdatesLayer { check: Some(false) });
let logging = globals.debug.then_some(CliLoggingLayer {
level: Some("debug".to_string()),
});
CliLayer {
output,
updates,
logging,
..CliLayer::default()
}
}
pub(crate) fn printer_from_verbosity(verbosity: OutputVerbosity) -> Printer {
match verbosity {
OutputVerbosity::Quiet => Printer::Quiet,
OutputVerbosity::Normal => Printer::Default,
OutputVerbosity::Verbose => Printer::Verbose,
}
}
pub(crate) fn require_no_json_override(process_local_json: bool) -> anyhow::Result<()> {
anyhow::ensure!(
!process_local_json,
"--json is not supported for this command"
);
Ok(())
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct StorageDirArgs {
/// Local storage directory (default: ~/.fabro/storage)

View file

@ -2,6 +2,8 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use fabro_config::merge::combine_files;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;
@ -34,34 +36,66 @@ pub(crate) struct CommandContext {
}
impl CommandContext {
pub(crate) fn base(printer: Printer) -> Result<Self> {
Self::new(printer, ServerMode::None)
pub(crate) fn base(
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(printer, ServerMode::None, cli_settings, cli_layer)
}
pub(crate) fn for_target(args: &ServerTargetArgs, printer: Printer) -> Result<Self> {
Self::new(printer, ServerMode::ByTarget {
target_override: args.server.clone(),
})
pub(crate) fn for_target(
args: &ServerTargetArgs,
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(
printer,
ServerMode::ByTarget {
target_override: args.server.clone(),
},
cli_settings,
cli_layer,
)
}
pub(crate) fn for_connection(args: &ServerConnectionArgs, printer: Printer) -> Result<Self> {
Self::new(printer, ServerMode::ByStorageDir {
target_override: args.target.server.clone(),
storage_dir_override: args.storage_dir.clone_path(),
})
pub(crate) fn for_connection(
args: &ServerConnectionArgs,
printer: Printer,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
Self::new(
printer,
ServerMode::ByStorageDir {
target_override: args.target.server.clone(),
storage_dir_override: args.storage_dir.clone_path(),
},
cli_settings,
cli_layer,
)
}
fn new(printer: Printer, server_mode: ServerMode) -> Result<Self> {
fn new(
printer: Printer,
server_mode: ServerMode,
cli_settings: CliSettings,
cli_layer: &CliLayer,
) -> Result<Self> {
let cwd = std::env::current_dir().context("Failed to get current directory")?;
let base_config_path = user_config::active_settings_path(None);
let machine_settings = match &server_mode {
let disk_settings = match &server_mode {
ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
ServerMode::ByStorageDir {
storage_dir_override,
..
} => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
};
let cli_settings = user_config::resolve_cli_settings(&machine_settings)?;
let machine_settings = combine_files(disk_settings, SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
Ok(Self {
printer,

View file

@ -1,15 +1,18 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::{ArtifactCpArgs, GlobalArgs};
use crate::args::ArtifactCpArgs;
use crate::server_client::ServerStoreClient;
use crate::shared::{print_json_pretty, split_run_path};
pub(super) async fn cp_command(
args: &ArtifactCpArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (run_id_selector, asset_path) = parse_source(&args.source);
@ -18,6 +21,8 @@ pub(super) async fn cp_command(
run_id_selector,
args.node.as_deref(),
args.retry,
cli,
cli_layer,
printer,
)
.await?;
@ -55,7 +60,7 @@ pub(super) async fn cp_command(
.unwrap_or_else(|| std::ffi::OsStr::new(&entry.relative_path)),
);
write_artifact_file(&client, &run_id, entry, &dest_file).await?;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"copied": [{
"relative_path": entry.relative_path,
@ -116,7 +121,7 @@ pub(super) async fn cp_command(
}
}
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "copied": copied }))?;
} else {
fabro_util::printout!(

View file

@ -1,14 +1,17 @@
use anyhow::Result;
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{ArtifactListArgs, GlobalArgs};
use crate::args::ArtifactListArgs;
pub(super) async fn list_command(
args: &ArtifactListArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (_run_id, _client, entries) = super::resolve_artifacts(
@ -16,11 +19,13 @@ pub(super) async fn list_command(
&args.run_id,
args.node.as_deref(),
args.retry,
cli,
cli_layer,
printer,
)
.await?;
if globals.json {
if cli.output.format == OutputFormat::Json {
fabro_util::printout!(printer, "{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}

View file

@ -2,10 +2,12 @@ mod cp;
mod list;
use anyhow::{Context, Result};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_types::{RunId, StageId};
use fabro_util::printer::Printer;
use crate::args::{ArtifactCommand, ArtifactNamespace, GlobalArgs, ServerTargetArgs};
use crate::args::{ArtifactCommand, ArtifactNamespace, ServerTargetArgs};
use crate::command_context::CommandContext;
use crate::server_client::ServerStoreClient;
use crate::server_runs::ServerSummaryLookup;
@ -25,9 +27,11 @@ pub(super) async fn resolve_artifacts(
run_selector: &str,
node: Option<&str>,
retry: Option<u32>,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(RunId, ServerStoreClient, Vec<ArtifactEntry>)> {
let ctx = CommandContext::for_target(server, printer)?;
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(run_selector)?;
let run_id = run.run_id();
@ -64,11 +68,12 @@ pub(super) async fn resolve_artifacts(
pub(crate) async fn dispatch(
ns: ArtifactNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match ns.command {
ArtifactCommand::List(args) => list::list_command(&args, globals, printer).await,
ArtifactCommand::Cp(args) => cp::cp_command(&args, globals, printer).await,
ArtifactCommand::List(args) => list::list_command(&args, cli, cli_layer, printer).await,
ArtifactCommand::Cp(args) => cp::cp_command(&args, cli, cli_layer, printer).await,
}
}

View file

@ -3,10 +3,11 @@ use std::path::Path;
use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode};
use fabro_config::{load_and_resolve, load_settings_project, project};
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SettingsArgs};
use crate::args::SettingsArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config;
@ -76,9 +77,11 @@ fn strip_nulls(value: &mut serde_json::Value) {
fn local_settings_value(
args: &SettingsArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
let base_ctx = CommandContext::base(printer)?;
let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let layers = config_layers(&base_ctx, args.workflow.as_deref())?;
let mut value = serde_json::to_value(load_and_resolve(
layers,
@ -91,15 +94,17 @@ fn local_settings_value(
async fn rendered_config(
args: &SettingsArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
if args.local {
return local_settings_value(args, printer);
return local_settings_value(args, cli, cli_layer, printer);
}
if args.workflow.is_some() {
anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`");
}
let ctx = CommandContext::for_target(&args.target, printer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
ctx.server()
.await?
.retrieve_resolved_server_settings()
@ -108,11 +113,12 @@ async fn rendered_config(
pub(crate) async fn execute(
args: &SettingsArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let config = Box::pin(rendered_config(args, printer)).await?;
if globals.json {
let config = Box::pin(rendered_config(args, cli, cli_layer, printer)).await?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&config)?;
return Ok(());
}

View file

@ -7,6 +7,8 @@ use fabro_config::user::{
active_settings_path, legacy_old_user_config_path, legacy_server_config_path,
legacy_user_config_path,
};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
pub(crate) use fabro_util::check_report::{
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
};
@ -14,7 +16,7 @@ use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::version::FABRO_VERSION;
use crate::args::{DoctorArgs, GlobalArgs};
use crate::args::DoctorArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config;
@ -231,11 +233,13 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer:
pub(crate) async fn run_doctor(
args: &DoctorArgs,
verbose: bool,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<i32, anyhow::Error> {
let styles = Styles::detect_stdout();
let spinner = if globals.json {
let json = cli.output.format == OutputFormat::Json;
let spinner = if json {
None
} else {
let spinner = indicatif::ProgressBar::new_spinner();
@ -290,7 +294,7 @@ pub(crate) async fn run_doctor(
}],
};
let ctx = match CommandContext::for_target(&args.target, printer) {
let ctx = match CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer) {
Ok(ctx) => ctx,
Err(err) => {
report.sections.push(CheckSection {
@ -311,7 +315,7 @@ pub(crate) async fn run_doctor(
spinner.finish_and_clear();
}
if globals.json {
if json {
print_json_pretty(&report)?;
} else {
render_report(&report, &styles, verbose, printer);
@ -341,7 +345,7 @@ pub(crate) async fn run_doctor(
spinner.finish_and_clear();
}
if globals.json {
if json {
print_json_pretty(&report)?;
} else {
render_report(&report, &styles, verbose, printer);
@ -370,7 +374,7 @@ pub(crate) async fn run_doctor(
spinner.finish_and_clear();
}
if globals.json {
if json {
print_json_pretty(&report)?;
} else {
render_report(&report, &styles, verbose, printer);
@ -411,7 +415,7 @@ pub(crate) async fn run_doctor(
spinner.finish_and_clear();
}
if globals.json {
if json {
print_json_pretty(&report)?;
} else {
render_report(&report, &styles, verbose, printer);

View file

@ -6,12 +6,12 @@ use fabro_agent::cli::{OutputFormat, run_with_args, run_with_args_and_client};
use fabro_llm::client::Client;
use fabro_llm::providers::FabroServerAdapter;
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_types::settings::InterpString;
use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat;
use fabro_types::settings::run::McpEntryLayer;
use fabro_types::settings::{CliSettings, InterpString};
use fabro_util::printer::Printer;
use crate::args::{ExecArgs, GlobalArgs};
use crate::args::ExecArgs;
use crate::user_config;
fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
@ -100,34 +100,28 @@ fn runtime_mcp_server(name: &str, entry: &McpEntryLayer) -> McpServerSettings {
pub(crate) async fn execute(
mut args: ExecArgs,
globals: &GlobalArgs,
cli: &CliSettings,
_printer: Printer,
) -> Result<()> {
use fabro_agent::cli::PermissionLevel as AgentPermissionLevel;
use fabro_types::settings::run::AgentPermissions;
let cli_settings = user_config::load_settings()?;
let resolved_cli = user_config::resolve_cli_settings(&cli_settings)?;
let raw_settings = user_config::load_settings()?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = crate::sleep_inhibitor::guard(resolved_cli.exec.prevent_idle_sleep);
let provider_str = resolved_cli
let _sleep_guard = crate::sleep_inhibitor::guard(cli.exec.prevent_idle_sleep);
let provider_str = cli
.exec
.model
.provider
.as_ref()
.map(InterpString::as_source);
let model_str = resolved_cli
.exec
.model
.name
.as_ref()
.map(InterpString::as_source);
let permissions = resolved_cli.exec.agent.permissions.map(|p| match p {
let model_str = cli.exec.model.name.as_ref().map(InterpString::as_source);
let permissions = cli.exec.agent.permissions.map(|p| match p {
AgentPermissions::ReadOnly => AgentPermissionLevel::ReadOnly,
AgentPermissions::ReadWrite => AgentPermissionLevel::ReadWrite,
AgentPermissions::Full => AgentPermissionLevel::Full,
});
let output_format = Some(match resolved_cli.output.format {
let output_format = Some(match cli.output.format {
SettingsOutputFormat::Text => OutputFormat::Text,
SettingsOutputFormat::Json => OutputFormat::Json,
});
@ -137,16 +131,12 @@ pub(crate) async fn execute(
permissions,
output_format,
);
if globals.json {
args.agent.output_format = Some(OutputFormat::Json);
}
let server_target = user_config::exec_server_target(&args.server, &cli_settings)?;
let server_target = user_config::exec_server_target(&args.server, &raw_settings)?;
// v2 MCPs live under `cli.exec.agent.mcps` (owner-specific) or
// `run.agent.mcps`. For `fabro exec` we use the cli.exec path, falling
// back to run.agent.mcps if unset.
let mcp_servers: Vec<McpServerSettings> = if !resolved_cli.exec.agent.mcps.is_empty() {
resolved_cli
.exec
let mcp_servers: Vec<McpServerSettings> = if !cli.exec.agent.mcps.is_empty() {
cli.exec
.agent
.mcps
.values()
@ -157,7 +147,7 @@ pub(crate) async fn execute(
tool_timeout_secs: server.tool_timeout_secs,
})
.collect()
} else if let Some(mcps) = cli_settings
} else if let Some(mcps) = raw_settings
.cli
.as_ref()
.and_then(|cli| cli.exec.as_ref())
@ -169,7 +159,7 @@ pub(crate) async fn execute(
.map(|(name, entry)| runtime_mcp_server(name, entry))
.collect()
} else {
fabro_config::resolve_run_from_file(&cli_settings)
fabro_config::resolve_run_from_file(&raw_settings)
.map(|settings| {
settings
.agent

View file

@ -4,12 +4,13 @@ use anyhow::bail;
use fabro_api::types;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use tracing::debug;
use crate::args::{GlobalArgs, GraphArgs, GraphDirection};
use crate::args::{GraphArgs, GraphDirection, require_no_json_override};
use crate::command_context::CommandContext;
use crate::commands::run::output::api_diagnostics_to_local;
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
@ -18,14 +19,16 @@ use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, r
pub(crate) async fn run(
args: &GraphArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> anyhow::Result<()> {
if globals.json && args.output.is_none() {
globals.require_no_json()?;
if process_local_json && args.output.is_none() {
require_no_json_override(process_local_json)?;
}
let ctx = CommandContext::for_target(&args.target, printer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),
@ -60,7 +63,7 @@ pub(crate) async fn run(
if let Some(ref output_path) = args.output {
std::fs::write(output_path, &rendered)?;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"path": absolute_or_current(output_path),
"format": args.format.to_string(),

View file

@ -18,6 +18,8 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f
use fabro_config::user::SETTINGS_CONFIG_FILENAME;
use fabro_config::{Storage, envfile, legacy_env};
use fabro_model::Provider;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::{dev_token, session_secret};
@ -32,8 +34,7 @@ use tokio::task::spawn_blocking;
use super::doctor;
use crate::args::{
DoctorArgs, GlobalArgs, InstallArgs, InstallGitHubStrategyArg, InstallNonInteractiveArgs,
ServerTargetArgs,
DoctorArgs, InstallArgs, InstallGitHubStrategyArg, InstallNonInteractiveArgs, ServerTargetArgs,
};
use crate::commands::server::{record, stop};
use crate::gh::GhCli;
@ -1229,15 +1230,18 @@ async fn persist_install_outputs_with_settings(
pub(crate) async fn run_install(
args: &InstallArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
if globals.json && !args.non_interactive {
let json = cli.output.format == OutputFormat::Json;
if process_local_json && !args.non_interactive {
bail!("--json is only supported for install with --non-interactive");
}
let result = Box::pin(run_install_inner(args, globals, printer)).await;
if globals.json {
let result = Box::pin(run_install_inner(args, cli, cli_layer, printer)).await;
if json {
let emit_result = match &result {
Ok(()) => emit_install_json_event(&install_complete_event()),
Err(err) => emit_install_json_event(&install_error_event(&err.to_string())),
@ -1252,9 +1256,11 @@ pub(crate) async fn run_install(
async fn run_install_inner(
args: &InstallArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
let web_url = &args.web_url;
let s = Styles::detect_stderr();
let emoji = console::Emoji("⚒️ ", "");
@ -1360,7 +1366,7 @@ async fn run_install_inner(
} else {
GitHubAppHandoffMode::Interactive
},
globals.json,
json,
printer,
)
.await?;
@ -1539,7 +1545,7 @@ async fn run_install_inner(
target: ServerTargetArgs::default(),
verbose: true,
};
let _ = doctor::run_doctor(&doctor_args, true, globals, printer).await?;
let _ = doctor::run_doctor(&doctor_args, true, cli, cli_layer, printer).await?;
}
fabro_util::printerr!(printer, "");

View file

@ -3,12 +3,14 @@ use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_api::{self, types as api_types};
use fabro_model::{Catalog, Model, Provider};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::args::{GlobalArgs, ModelListArgs, ModelTestArgs, ModelsCommand};
use crate::args::{ModelListArgs, ModelTestArgs, ModelsCommand};
use crate::command_context::CommandContext;
use crate::server_client;
@ -41,7 +43,8 @@ struct ModelTestOutput {
pub(crate) async fn execute(
command: Option<ModelsCommand>,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let command = command.unwrap_or_default();
@ -49,10 +52,15 @@ pub(crate) async fn execute(
ModelsCommand::List(args) => &args.target,
ModelsCommand::Test(args) => &args.target,
};
let ctx = CommandContext::for_target(target_args, printer)?;
let ctx = CommandContext::for_target(target_args, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
run_models(command, server.api(), globals.json).await
run_models(
command,
server.api(),
cli.output.format == OutputFormat::Json,
)
.await
}
fn format_context_window(tokens: i64) -> String {

View file

@ -2,13 +2,13 @@ use std::io::Write;
use fabro_config::project::resolve_workflow;
use fabro_graphviz::parser::parse_ast;
use fabro_types::settings::CliSettings;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, ParseArgs};
use crate::args::ParseArgs;
use crate::shared::read_workflow_file;
pub(crate) fn run(args: &ParseArgs, globals: &GlobalArgs, _printer: Printer) -> anyhow::Result<()> {
let _ = globals;
pub(crate) fn run(args: &ParseArgs, _cli: &CliSettings, _printer: Printer) -> anyhow::Result<()> {
let stdout = std::io::stdout();
run_to(args, stdout.lock())
}

View file

@ -1,18 +1,22 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{GlobalArgs, PrCloseArgs};
use crate::args::PrCloseArgs;
use crate::shared::print_json_pretty;
pub(super) async fn close_command(
args: PrCloseArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(printer)?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
fabro_github::close_pull_request(
&creds,
@ -25,7 +29,7 @@ pub(super) async fn close_command(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, "Closed pull request");
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,

View file

@ -1,12 +1,14 @@
use anyhow::{Context, Result, bail};
use fabro_model::Catalog;
use fabro_sandbox::daytona::detect_repo_info;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_workflow::outcome::StageStatus;
use fabro_workflow::pull_request::maybe_open_pull_request;
use tracing::info;
use crate::args::{GlobalArgs, PrCreateArgs};
use crate::args::PrCreateArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::server_runs::ServerSummaryLookup;
@ -15,10 +17,11 @@ use crate::shared::repo::ensure_matching_repo_origin;
pub(super) async fn create_command(
args: PrCreateArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run_id)?;
let run_id = run.run_id();
@ -74,7 +77,7 @@ pub(super) async fn create_command(
let (owner, repo) = fabro_github::parse_github_owner_repo(&https_url)
.map_err(|err| anyhow::anyhow!("{err}"))?;
let creds = super::load_github_credentials_required(printer)?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let branch_found = fabro_github::branch_exists(
&creds,
@ -116,14 +119,14 @@ pub(super) async fn create_command(
match record {
Some(record) => {
info!(pr_url = %record.html_url, "Pull request created");
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&record)?;
} else {
fabro_util::printout!(printer, "{}", record.html_url);
}
}
None => {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::Value::Null)?;
} else {
fabro_util::printout!(printer, "No pull request created (empty diff).");

View file

@ -1,13 +1,15 @@
use anyhow::Result;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use futures::future::join_all;
use serde::Serialize;
use tracing::info;
use crate::args::{GlobalArgs, PrListArgs};
use crate::args::PrListArgs;
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::{color_if, print_json_pretty};
@ -23,10 +25,11 @@ struct PrRow {
pub(super) async fn list_command(
args: PrListArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let mut entries = Vec::new();
@ -39,7 +42,7 @@ pub(super) async fn list_command(
}
if entries.is_empty() {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&Vec::<PrRow>::new())?;
return Ok(());
}
@ -47,7 +50,7 @@ pub(super) async fn list_command(
return Ok(());
}
let creds = super::load_github_credentials_required(printer)?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let futures: Vec<_> = entries
.iter()
@ -101,7 +104,7 @@ pub(super) async fn list_command(
.collect()
};
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&rows)?;
return Ok(());
}

View file

@ -1,18 +1,22 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{GlobalArgs, PrMergeArgs};
use crate::args::PrMergeArgs;
use crate::shared::print_json_pretty;
pub(super) async fn merge_command(
args: PrMergeArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(printer)?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
fabro_github::merge_pull_request(
&creds,
@ -26,7 +30,7 @@ pub(super) async fn merge_command(
.map_err(|err| anyhow::anyhow!("{err}"))?;
info!(number = record.number, owner = %record.owner, repo = %record.repo, method = %args.method, "Merged pull request");
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"number": record.number,
"html_url": record.html_url,

View file

@ -8,10 +8,11 @@ use anyhow::{Context, Result, anyhow};
use fabro_config::Storage;
use fabro_github::GitHubCredentials;
use fabro_types::PullRequestRecord;
use fabro_types::settings::InterpString;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliSettings, InterpString};
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, PrCommand, PrNamespace, ServerTargetArgs};
use crate::args::{PrCommand, PrNamespace, ServerTargetArgs};
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::github::build_github_credentials;
@ -22,20 +23,27 @@ const GITHUB_CREDENTIALS_REQUIRED: &str =
pub(crate) async fn dispatch(
ns: PrNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match ns.command {
PrCommand::Create(args) => Box::pin(create::create_command(args, globals, printer)).await,
PrCommand::List(args) => list::list_command(args, globals, printer).await,
PrCommand::View(args) => view::view_command(args, globals, printer).await,
PrCommand::Merge(args) => merge::merge_command(args, globals, printer).await,
PrCommand::Close(args) => close::close_command(args, globals, printer).await,
PrCommand::Create(args) => {
Box::pin(create::create_command(args, cli, cli_layer, printer)).await
}
PrCommand::List(args) => list::list_command(args, cli, cli_layer, printer).await,
PrCommand::View(args) => view::view_command(args, cli, cli_layer, printer).await,
PrCommand::Merge(args) => merge::merge_command(args, cli, cli_layer, printer).await,
PrCommand::Close(args) => close::close_command(args, cli, cli_layer, printer).await,
}
}
fn load_github_credentials_required(printer: Printer) -> Result<GitHubCredentials> {
let ctx = CommandContext::base(printer)?;
fn load_github_credentials_required(
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<GitHubCredentials> {
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
let server_settings =
fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| {
anyhow!(
@ -68,9 +76,11 @@ fn load_github_credentials_required(printer: Printer) -> Result<GitHubCredential
pub(crate) async fn load_pr_record(
server: &ServerTargetArgs,
run_id: &str,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(PullRequestRecord, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer)?;
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(run_id)?;
let run_id = run.run_id();

View file

@ -1,18 +1,22 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{GlobalArgs, PrViewArgs};
use crate::args::PrViewArgs;
use crate::shared::print_json_pretty;
pub(super) async fn view_command(
args: PrViewArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let (record, _run_id) = super::load_pr_record(&args.server, &args.run_id, printer).await?;
let (record, _run_id) =
super::load_pr_record(&args.server, &args.run_id, cli, cli_layer, printer).await?;
let creds = super::load_github_credentials_required(printer)?;
let creds = super::load_github_credentials_required(cli, cli_layer, printer)?;
let detail = fabro_github::get_pull_request(
&creds,
@ -26,7 +30,7 @@ pub(super) async fn view_command(
info!(number = detail.number, owner = %record.owner, repo = %record.repo, "Viewing pull request");
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&detail)?;
return Ok(());
}

View file

@ -1,11 +1,12 @@
use anyhow::bail;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, PreflightArgs};
use crate::args::PreflightArgs;
use crate::command_context::CommandContext;
use crate::commands::run::output::{
api_check_report_to_local, api_diagnostics_to_local, print_preflight_workflow_summary,
@ -16,12 +17,13 @@ use crate::shared::print_json_pretty;
pub(crate) async fn execute(
mut args: PreflightArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&args.target, printer)?;
args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
let manifest = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
@ -36,7 +38,7 @@ pub(crate) async fn execute(
let response = client.run_preflight(manifest.manifest).await?;
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&response)?;
} else {
print_preflight_workflow_summary(

View file

@ -2,21 +2,25 @@ use anyhow::Result;
use fabro_api::types;
use fabro_auth::credential_id_for;
use fabro_config::legacy_env;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, ProviderLoginArgs};
use crate::args::{ProviderLoginArgs, require_no_json_override};
use crate::command_context::CommandContext;
use crate::shared::provider_auth;
pub(super) async fn login_command(
args: ProviderLoginArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
globals.require_no_json()?;
require_no_json_override(process_local_json)?;
let s = Styles::detect_stderr();
let ctx = CommandContext::for_target(&args.target, printer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
let credential = if args.api_key_stdin {
provider_auth::authenticate_provider_with_api_key_source(

View file

@ -1,16 +1,22 @@
mod login;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, ProviderCommand, ProviderNamespace};
use crate::args::{ProviderCommand, ProviderNamespace};
pub(crate) async fn dispatch(
ns: ProviderNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
match ns.command {
ProviderCommand::Login(args) => login::login_command(args, globals, printer).await,
ProviderCommand::Login(args) => {
login::login_command(args, cli, cli_layer, process_local_json, printer).await
}
}
}

View file

@ -1,9 +1,9 @@
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::GlobalArgs;
pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result<Vec<String>> {
pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result<Vec<String>> {
let repo_root = super::init::git_repo_root()?;
let mut removed = Vec::new();
@ -20,7 +20,7 @@ pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result<Vec<S
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
removed.push(".fabro/".to_string());
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -29,7 +29,7 @@ pub(crate) fn run_deinit(globals: &GlobalArgs, printer: Printer) -> Result<Vec<S
);
}
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
"\n{}",

View file

@ -1,11 +1,13 @@
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::process::Command as TokioCommand;
use tokio::task::spawn_blocking;
use crate::args::{GlobalArgs, RepoInitArgs, ServerTargetArgs};
use crate::args::{RepoInitArgs, ServerTargetArgs};
use crate::command_context::CommandContext;
#[expect(
@ -29,7 +31,8 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
pub(crate) async fn run_init(
args: &RepoInitArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<Vec<String>> {
let repo_root = spawn_blocking(git_repo_root)
@ -71,7 +74,7 @@ draft = true
let green = console::Style::new().green();
let bold = console::Style::new().bold();
let dim = console::Style::new().dim();
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -104,7 +107,7 @@ draft = true
)
.with_context(|| format!("failed to write {}", dot_path.display()))?;
created.push(".fabro/workflows/hello/workflow.fabro".to_string());
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -121,7 +124,7 @@ draft = true
)
.with_context(|| format!("failed to write {}", toml_path.display()))?;
created.push(".fabro/workflows/hello/workflow.toml".to_string());
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
" {} {}",
@ -130,7 +133,7 @@ draft = true
);
}
if !globals.json {
if cli.output.format != OutputFormat::Json {
fabro_util::printerr!(
printer,
"\n{} Run a workflow with:\n\n {}",
@ -142,14 +145,19 @@ draft = true
);
}
if !globals.json {
check_github_app_installation(&args.target, printer).await;
if cli.output.format != OutputFormat::Json {
check_github_app_installation(&args.target, cli, cli_layer, printer).await;
}
Ok(created)
}
async fn check_github_app_installation(target: &ServerTargetArgs, printer: Printer) {
async fn check_github_app_installation(
target: &ServerTargetArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) {
// Get the git remote origin URL
let output = match TokioCommand::new("git")
.args(["remote", "get-url", "origin"])
@ -187,7 +195,7 @@ async fn check_github_app_installation(target: &ServerTargetArgs, printer: Print
return; // Not a GitHub repo — skip silently
};
let ctx = match CommandContext::for_target(target, printer) {
let ctx = match CommandContext::for_target(target, printer, cli.clone(), cli_layer) {
Ok(ctx) => ctx,
Err(err) => {
fabro_util::printerr!(

View file

@ -2,27 +2,30 @@ pub(crate) mod deinit;
pub(crate) mod init;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, RepoCommand, RepoNamespace};
use crate::args::{RepoCommand, RepoNamespace};
use crate::shared::print_json_pretty;
pub(crate) async fn dispatch(
ns: RepoNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match ns.command {
RepoCommand::Init(args) => {
let created = init::run_init(&args, globals, printer).await?;
if globals.json {
let created = init::run_init(&args, cli, cli_layer, printer).await?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "created": created }))?;
}
Ok(())
}
RepoCommand::Deinit => {
let removed = deinit::run_deinit(globals, printer)?;
if globals.json {
let removed = deinit::run_deinit(cli, printer)?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "removed": removed }))?;
}
Ok(())

View file

@ -1,27 +1,34 @@
use anyhow::Result;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat, OutputVerbosity};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, RunArgs};
use crate::args::RunArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
use crate::user_config::settings_layer_with_storage_dir;
pub(crate) async fn execute(
mut args: RunArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&args.target, printer)?;
let cli = settings_layer_with_storage_dir(None)?;
args.verbose = args.verbose || ctx.cli_settings().output.verbosity == OutputVerbosity::Verbose;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let cli_defaults = settings_layer_with_storage_dir(None)?;
args.verbose = args.verbose || cli.output.verbosity == OutputVerbosity::Verbose;
let quiet = args.detach;
let prevent_idle_sleep = ctx.cli_settings().exec.prevent_idle_sleep;
let created_run = Box::pin(super::create::create_run(
&ctx, &args, cli, styles, quiet, printer,
&ctx,
&args,
cli_defaults,
styles,
quiet,
printer,
))
.await?;
@ -34,8 +41,9 @@ pub(crate) async fn execute(
let client = ctx.server().await?;
super::start::start_run_with_client(&client, &created_run.run_id, false).await?;
let json = cli.output.format == OutputFormat::Json;
if args.detach {
if globals.json {
if json {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
} else {
fabro_util::printout!(printer, "{}", created_run.run_id);
@ -46,11 +54,11 @@ pub(crate) async fn execute(
&created_run.run_id,
true,
styles,
globals.json,
json,
printer,
)
.await?;
if !globals.json {
if !json {
super::output::print_run_summary_with_client(
&client,
&created_run.run_id,

View file

@ -1,11 +1,13 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tokio::fs;
use tracing::{debug, info};
use crate::args::{CpArgs, GlobalArgs, ServerTargetArgs};
use crate::args::{CpArgs, ServerTargetArgs};
use crate::command_context::CommandContext;
use crate::server_client::ServerStoreClient;
use crate::server_runs::ServerSummaryLookup;
@ -25,7 +27,12 @@ enum CopyDirection {
},
}
pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> {
pub(crate) async fn cp_command(
args: CpArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let direction = parse_direction(&args.src, &args.dst)?;
match direction {
@ -35,7 +42,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin
local_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, printer).await?;
resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer)
.await?;
let file_count = if args.recursive {
Some(download_recursive(&client, &run_id, &remote_path, &local_path).await?)
@ -45,7 +53,7 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin
None
};
if globals.json {
if cli.output.format == OutputFormat::Json {
let mut value = serde_json::json!({
"direction": "download",
"recursive": args.recursive,
@ -66,7 +74,8 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin
remote_path,
} => {
let (client, run_id) =
resolve_client_and_run_id(&args.server, &run_prefix, printer).await?;
resolve_client_and_run_id(&args.server, &run_prefix, cli, cli_layer, printer)
.await?;
let file_count = if args.recursive {
Some(upload_recursive(&client, &run_id, &local_path, &remote_path).await?)
@ -76,7 +85,7 @@ pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs, printer: Prin
None
};
if globals.json {
if cli.output.format == OutputFormat::Json {
let mut value = serde_json::json!({
"direction": "upload",
"recursive": args.recursive,
@ -120,9 +129,11 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
async fn resolve_client_and_run_id(
server: &ServerTargetArgs,
run_prefix: &str,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<(ServerStoreClient, fabro_types::RunId)> {
let ctx = CommandContext::for_target(server, printer)?;
let ctx = CommandContext::for_target(server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(run_prefix)?;
Ok((lookup.client().clone_for_reuse(), run.run_id()))

View file

@ -1,18 +1,25 @@
use std::io::{self, IsTerminal, Write};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
use crate::args::{DiffArgs, GlobalArgs};
use crate::args::DiffArgs;
use crate::command_context::CommandContext;
use crate::server_client::RunProjection;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::print_json_pretty;
pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> {
pub(crate) async fn run(
args: DiffArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
info!(run_id = %args.run, "Showing diff");
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let run_id = run.run_id();
@ -20,7 +27,7 @@ pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs, printer: Printer)
let patch = resolve_diff(&state, &args)?;
if globals.json {
if cli.output.format == OutputFormat::Json {
let value = serde_json::json!({
"run_id": run_id,
"node": args.node,

View file

@ -1,11 +1,13 @@
use anyhow::{Context, Result};
use fabro_checkpoint::git::Store;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_workflow::operations::{ForkRunInput, RewindTarget, build_timeline_or_rebuild, fork};
use git2::Repository;
use crate::args::{ForkArgs, GlobalArgs};
use crate::args::ForkArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::server_runs::ServerSummaryLookup;
@ -15,11 +17,12 @@ use crate::shared::repo::ensure_matching_repo_origin;
pub(crate) async fn run(
args: &ForkArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run_id)?;
let run_id = run.run_id();
@ -33,7 +36,7 @@ pub(crate) async fn run(
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
if args.list {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&super::rewind::timeline_entries_json(&timeline))?;
return Ok(());
}
@ -55,7 +58,7 @@ pub(crate) async fn run(
let run_id_string = run_id.to_string();
let new_run_id_string = new_run_id.to_string();
if globals.json {
if cli.output.format == OutputFormat::Json {
let target = args.target.clone().unwrap_or_else(|| "latest".to_string());
print_json_pretty(&serde_json::json!({
"source_run_id": run_id_string,

View file

@ -4,6 +4,8 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use chrono::{DateTime, Utc};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::json::normalize_json_value;
use fabro_util::printer::Printer;
use fabro_util::redact::redact_jsonl_line;
@ -11,7 +13,7 @@ use fabro_util::terminal::Styles;
use tokio::time;
use tracing::{debug, info};
use crate::args::{GlobalArgs, LogsArgs};
use crate::args::LogsArgs;
use crate::command_context::CommandContext;
use crate::server_client;
use crate::server_runs::ServerSummaryLookup;
@ -22,10 +24,11 @@ const FOLLOW_TERMINAL_GRACE: Duration = Duration::from_millis(500);
pub(crate) async fn run(
args: &LogsArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let client = lookup.client();
@ -52,7 +55,7 @@ pub(crate) async fn run(
let stdout = io::stdout();
let is_tty = stdout.is_terminal();
let mut out = stdout.lock();
let pretty = args.pretty && !globals.json;
let pretty = args.pretty && cli.output.format != OutputFormat::Json;
for line in &filtered {
if pretty {

View file

@ -1,8 +1,10 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{AttachArgs, GlobalArgs, RunArgs, RunCommands, RunWorkerArgs, StartArgs};
use crate::args::{AttachArgs, RunCommands, RunWorkerArgs, StartArgs};
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::print_json_pretty;
@ -26,30 +28,29 @@ pub(crate) mod ssh;
pub(crate) mod start;
pub(crate) mod wait;
fn apply_json_defaults(args: &mut RunArgs, globals: &GlobalArgs) {
if globals.json {
args.auto_approve = true;
}
}
pub(crate) async fn dispatch(
cmd: RunCommands,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
_process_local_json: bool,
printer: Printer,
) -> Result<()> {
match cmd {
RunCommands::Run(mut args) => {
apply_json_defaults(&mut args, globals);
Box::pin(command::execute(args, globals, printer)).await
}
RunCommands::Create(mut args) => {
apply_json_defaults(&mut args, globals);
RunCommands::Run(args) => Box::pin(command::execute(args, cli, cli_layer, printer)).await,
RunCommands::Create(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = settings_layer_with_storage_dir(None)?;
let ctx = CommandContext::for_target(&args.target, printer)?;
let created_run =
Box::pin(create::create_run(&ctx, &args, cli, styles, true, printer)).await?;
if globals.json {
let cli_defaults = settings_layer_with_storage_dir(None)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let created_run = Box::pin(create::create_run(
&ctx,
&args,
cli_defaults,
styles,
true,
printer,
))
.await?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
} else {
fabro_util::printout!(printer, "{}", created_run.run_id);
@ -57,19 +58,19 @@ pub(crate) async fn dispatch(
Ok(())
}
RunCommands::Start(StartArgs { server, run }) => {
let ctx = CommandContext::for_target(&server, printer)?;
let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run_info = lookup.resolve(&run)?;
let run_id = run_info.run_id();
start::start_run_with_client(lookup.client(), &run_id, false).await?;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
}
Ok(())
}
RunCommands::Attach(AttachArgs { server, run }) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let ctx = CommandContext::for_target(&server, printer)?;
let ctx = CommandContext::for_target(&server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run_info = lookup.resolve(&run)?;
let run_id = run_info.run_id();
@ -78,7 +79,7 @@ pub(crate) async fn dispatch(
&run_id,
false,
styles,
globals.json,
cli.output.format == OutputFormat::Json,
printer,
)
.await?;
@ -105,31 +106,32 @@ pub(crate) async fn dispatch(
)
.await
}
RunCommands::Diff(args) => diff::run(args, globals, printer).await,
RunCommands::Diff(args) => diff::run(args, cli, cli_layer, printer).await,
RunCommands::Logs(args) => {
let styles = Styles::detect_stdout();
logs::run(&args, &styles, globals, printer).await
logs::run(&args, &styles, cli, cli_layer, printer).await
}
RunCommands::Resume(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx =
CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
crate::sleep_inhibitor::guard(ctx.cli_settings().exec.prevent_idle_sleep)
};
resume::resume_command(args, styles, globals, printer).await
resume::resume_command(args, styles, cli, cli_layer, printer).await
}
RunCommands::Rewind(args) => {
let styles = Styles::detect_stderr();
Box::pin(rewind::run(&args, &styles, globals, printer)).await
Box::pin(rewind::run(&args, &styles, cli, cli_layer, printer)).await
}
RunCommands::Fork(args) => {
let styles = Styles::detect_stderr();
Box::pin(fork::run(&args, &styles, globals, printer)).await
Box::pin(fork::run(&args, &styles, cli, cli_layer, printer)).await
}
RunCommands::Wait(args) => {
let styles = Styles::detect_stderr();
wait::run(&args, &styles, globals, printer).await
wait::run(&args, &styles, cli, cli_layer, printer).await
}
}
}

View file

@ -1,14 +1,22 @@
use anyhow::{Context, Result};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{GlobalArgs, PreviewArgs};
use crate::args::PreviewArgs;
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::print_json_pretty;
pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
pub(crate) async fn run(
args: PreviewArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let run_id = run.run_id();
@ -26,7 +34,7 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printe
info!(run_id = %args.run, port = args.port, "Generating preview URL");
if globals.json {
if cli.output.format == OutputFormat::Json {
match response.token {
Some(token) => {
print_json_pretty(&serde_json::json!({ "url": response.url, "token": token }))?;
@ -51,7 +59,7 @@ pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs, printer: Printe
}
}
if args.open && !globals.json {
if args.open && !process_local_json {
#[expect(
clippy::disallowed_methods,
reason = "Preview URL opening is a fire-and-forget OS integration, not a Tokio-managed child process."

View file

@ -1,7 +1,9 @@
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, ResumeArgs};
use crate::args::ResumeArgs;
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::print_json_pretty;
@ -14,18 +16,20 @@ use crate::shared::print_json_pretty;
pub(crate) async fn resume_command(
args: ResumeArgs,
styles: &'static Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let run_id = run.run_id();
super::start::start_run_with_client(lookup.client(), &run_id, true).await?;
let json = cli.output.format == OutputFormat::Json;
if args.detach {
if globals.json {
if json {
print_json_pretty(&serde_json::json!({ "run_id": run_id }))?;
} else {
fabro_util::printout!(printer, "{run_id}");
@ -36,11 +40,11 @@ pub(crate) async fn resume_command(
&run_id,
true,
styles,
globals.json,
json,
printer,
)
.await?;
if !globals.json {
if !json {
super::output::print_run_summary_with_client(
lookup.client(),
&run_id,

View file

@ -3,6 +3,8 @@ use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_checkpoint::git::Store;
use fabro_types::run_event::{CheckpointCompletedProps, RunRewoundProps, RunSubmittedProps};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::{EventBody, RunEvent};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -13,7 +15,7 @@ use fabro_workflow::operations::{
use git2::Repository;
use serde::Serialize;
use crate::args::{GlobalArgs, RewindArgs};
use crate::args::RewindArgs;
use crate::command_context::CommandContext;
use crate::commands::store::rebuild::rebuild_run_store;
use crate::server_client::ServerStoreClient;
@ -32,11 +34,12 @@ pub(crate) struct TimelineEntryJson {
pub(crate) async fn run(
args: &RewindArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run_id)?;
let run_id = run.run_id();
@ -50,7 +53,7 @@ pub(crate) async fn run(
let timeline = build_timeline_or_rebuild(&store, Some(&run_store), &run_id).await?;
if args.list || args.target.is_none() {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&timeline_entries_json(&timeline))?;
return Ok(());
}
@ -70,7 +73,7 @@ pub(crate) async fn run(
let run_id_string = run_id.to_string();
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"run_id": run_id_string,
"target": args.target.as_deref().unwrap(),

View file

@ -1,18 +1,26 @@
use anyhow::{Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::info;
use crate::args::{GlobalArgs, SshArgs};
use crate::args::{SshArgs, require_no_json_override};
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::print_json_pretty;
pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs, printer: Printer) -> Result<()> {
if globals.json && !args.print {
globals.require_no_json()?;
pub(crate) async fn run(
args: SshArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
if process_local_json && !args.print {
require_no_json_override(process_local_json)?;
}
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let run_id = run.run_id();
@ -24,7 +32,7 @@ pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs, printer: Printer) -
info!(run_id = %args.run, ttl_minutes = args.ttl, "Creating SSH access");
if args.print {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "command": ssh.command }))?;
} else {
{

View file

@ -2,6 +2,8 @@ use std::io::Write;
use anyhow::{Result, bail};
use fabro_types::RunId;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_workflow::records::Conclusion;
@ -9,7 +11,7 @@ use fabro_workflow::run_status::RunStatus;
use tokio::time;
use tracing::info;
use crate::args::{GlobalArgs, WaitArgs};
use crate::args::WaitArgs;
use crate::command_context::CommandContext;
use crate::server_runs::ServerSummaryLookup;
use crate::shared::{format_duration_ms, format_usd_micros};
@ -22,10 +24,11 @@ const WAIT_STARTUP_GRACE: std::time::Duration = std::time::Duration::from_secs(3
pub(crate) async fn run(
args: &WaitArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run_info = lookup.resolve(&args.run)?;
let client = lookup.client();
@ -74,7 +77,7 @@ pub(crate) async fn run(
let conclusion = client.get_run_state(&run_id).await?.conclusion;
if globals.json {
if cli.output.format == OutputFormat::Json {
let json_value = build_json_output(final_status, &run_id, conclusion.as_ref());
let mut out = std::io::stdout().lock();
serde_json::to_writer_pretty(&mut out, &json_value)?;

View file

@ -1,9 +1,11 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_workflow::run_status::RunStatus;
use serde::Serialize;
use crate::args::{GlobalArgs, InspectArgs};
use crate::args::InspectArgs;
use crate::command_context::CommandContext;
use crate::server_client::RunProjection;
use crate::server_runs::{ServerRunSummaryInfo, ServerSummaryLookup};
@ -19,8 +21,13 @@ pub(crate) struct InspectOutput {
pub sandbox: Option<serde_json::Value>,
}
pub(crate) async fn run(args: &InspectArgs, _globals: &GlobalArgs, printer: Printer) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
pub(crate) async fn run(
args: &InspectArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let run = lookup.resolve(&args.run)?;
let run_id = run.run_id();

View file

@ -4,13 +4,15 @@ use anyhow::Result;
use chrono::Utc;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_util::text::strip_goal_decoration;
use fabro_workflow::run_status::RunStatus;
use super::short_run_id;
use crate::args::{GlobalArgs, RunsListArgs};
use crate::args::RunsListArgs;
use crate::command_context::CommandContext;
use crate::server_runs::{ServerSummaryLookup, filter_server_runs};
use crate::shared::{color_if, format_duration_ms, tilde_path};
@ -18,10 +20,11 @@ use crate::shared::{color_if, format_duration_ms, tilde_path};
pub(crate) async fn list_command(
args: &RunsListArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_server_runs(
@ -32,7 +35,7 @@ pub(crate) async fn list_command(
!args.all,
);
if globals.json {
if cli.output.format == OutputFormat::Json {
let json_rows: Vec<_> = filtered
.iter()
.map(|run| {

View file

@ -1,8 +1,10 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, RunsCommands};
use crate::args::RunsCommands;
pub(crate) mod inspect;
pub(crate) mod list;
@ -10,16 +12,17 @@ pub(crate) mod rm;
pub(crate) async fn dispatch(
cmd: RunsCommands,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match cmd {
RunsCommands::Ps(args) => {
let styles = Styles::detect_stdout();
list::list_command(&args, &styles, globals, printer).await
list::list_command(&args, &styles, cli, cli_layer, printer).await
}
RunsCommands::Rm(args) => rm::remove_command(&args, globals, printer).await,
RunsCommands::Inspect(args) => inspect::run(&args, globals, printer).await,
RunsCommands::Rm(args) => rm::remove_command(&args, cli, cli_layer, printer).await,
RunsCommands::Inspect(args) => inspect::run(&args, cli, cli_layer, printer).await,
}
}

View file

@ -1,8 +1,10 @@
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use super::short_run_id;
use crate::args::{GlobalArgs, RunsRemoveArgs};
use crate::args::RunsRemoveArgs;
use crate::command_context::CommandContext;
use crate::server_client;
use crate::server_runs::{
@ -12,21 +14,23 @@ use crate::shared::print_json_pretty;
pub(crate) async fn remove_command(
args: &RunsRemoveArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&args.server, printer)?;
let ctx = CommandContext::for_target(&args.server, printer, cli.clone(), cli_layer)?;
let lookup = ServerSummaryLookup::from_client(ctx.server().await?).await?;
remove_from(args, lookup.client(), lookup.runs(), globals, printer).await
remove_from(args, lookup.client(), lookup.runs(), cli, printer).await
}
async fn remove_from(
args: &RunsRemoveArgs,
client: &server_client::ServerStoreClient,
runs: &[ServerRunSummaryInfo],
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
let mut had_errors = false;
let mut removed = Vec::new();
let mut errors = Vec::new();
@ -35,7 +39,7 @@ async fn remove_from(
let run = match resolve_server_run_from_summaries(runs, identifier) {
Ok(run) => run,
Err(err) => {
if !globals.json {
if !json {
fabro_util::printerr!(printer, "error: {identifier}: {err}");
}
errors.push(serde_json::json!({
@ -54,7 +58,7 @@ async fn remove_from(
short_run_id(&run_id),
run.status()
);
if !globals.json {
if !json {
fabro_util::printerr!(printer, "{error}");
}
errors.push(serde_json::json!({
@ -67,7 +71,7 @@ async fn remove_from(
let run_id = run.run_id().to_string();
if let Err(err) = delete_server_run(client, &run).await {
if !globals.json {
if !json {
fabro_util::printerr!(printer, "error: {identifier}: {err}");
}
errors.push(serde_json::json!({
@ -78,12 +82,12 @@ async fn remove_from(
continue;
}
removed.push(run_id.clone());
if !globals.json {
if !json {
fabro_util::printerr!(printer, "{}", short_run_id(&run_id));
}
}
if globals.json {
if json {
print_json_pretty(&serde_json::json!({
"removed": removed,
"errors": errors,

View file

@ -1,16 +1,24 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SandboxCommand};
use crate::args::SandboxCommand;
pub(crate) async fn dispatch(
command: SandboxCommand,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
process_local_json: bool,
printer: Printer,
) -> Result<()> {
match command {
SandboxCommand::Cp(args) => super::run::cp::cp_command(args, globals, printer).await,
SandboxCommand::Preview(args) => super::run::preview::run(args, globals, printer).await,
SandboxCommand::Ssh(args) => super::run::ssh::run(args, globals, printer).await,
SandboxCommand::Cp(args) => super::run::cp::cp_command(args, cli, cli_layer, printer).await,
SandboxCommand::Preview(args) => {
super::run::preview::run(args, cli, cli_layer, process_local_json, printer).await
}
SandboxCommand::Ssh(args) => {
super::run::ssh::run(args, cli, cli_layer, process_local_json, printer).await
}
}
}

View file

@ -3,10 +3,12 @@ use chrono::{DateTime, Utc};
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_api::Client;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, SecretListArgs};
use crate::args::SecretListArgs;
use crate::server_client;
use crate::shared::print_json_pretty;
@ -24,7 +26,7 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
pub(super) async fn list_command(
client: &Client,
_args: &SecretListArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let response = client
@ -33,7 +35,7 @@ pub(super) async fn list_command(
.await
.map_err(server_client::map_api_error)?;
let secrets = response.into_inner().data;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&secrets)?;
return Ok(());
}

View file

@ -3,23 +3,24 @@ mod rm;
mod set;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SecretCommand, SecretNamespace};
use crate::args::{SecretCommand, SecretNamespace};
use crate::command_context::CommandContext;
pub(crate) async fn dispatch(
ns: SecretNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_target(&ns.target, printer)?;
let ctx = CommandContext::for_target(&ns.target, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
match ns.command {
SecretCommand::List(args) => {
list::list_command(server.api(), &args, globals, printer).await
}
SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, globals, printer).await,
SecretCommand::Set(args) => set::set_command(server.api(), &args, globals, printer).await,
SecretCommand::List(args) => list::list_command(server.api(), &args, cli, printer).await,
SecretCommand::Rm(args) => rm::rm_command(server.api(), &args, cli, printer).await,
SecretCommand::Set(args) => set::set_command(server.api(), &args, cli, printer).await,
}
}

View file

@ -1,15 +1,17 @@
use anyhow::Result;
use fabro_api::{Client, types};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SecretRmArgs};
use crate::args::SecretRmArgs;
use crate::server_client;
use crate::shared::print_json_pretty;
pub(super) async fn rm_command(
client: &Client,
args: &SecretRmArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
client
@ -20,7 +22,7 @@ pub(super) async fn rm_command(
.send()
.await
.map_err(server_client::map_api_error)?;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({ "key": args.key }))?;
} else {
fabro_util::printerr!(printer, "Removed {}", args.key);

View file

@ -1,8 +1,10 @@
use anyhow::Result;
use fabro_api::{Client, types};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SecretSetArgs, SecretTypeArg};
use crate::args::{SecretSetArgs, SecretTypeArg};
use crate::server_client;
use crate::shared::print_json_pretty;
@ -16,7 +18,7 @@ fn api_secret_type(secret_type: SecretTypeArg) -> types::SecretType {
pub(super) async fn set_command(
client: &Client,
args: &SecretSetArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let meta = client
@ -31,7 +33,7 @@ pub(super) async fn set_command(
.await
.map_err(server_client::map_api_error)?
.into_inner();
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&meta)?;
} else {
fabro_util::printerr!(printer, "Set {}", meta.name);

View file

@ -6,6 +6,8 @@ use bytes::Bytes;
#[cfg(test)]
use fabro_store::{ArtifactStore, RunDatabase};
use fabro_store::{EventEnvelope, RunProjection, StageId};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_types::{RunBlobId, RunId};
use fabro_util::printer::Printer;
use fabro_workflow::run_dump::RunDump;
@ -13,7 +15,7 @@ use futures::future::BoxFuture;
#[cfg(test)]
use serde::de::DeserializeOwned;
use crate::args::{GlobalArgs, StoreDumpArgs};
use crate::args::StoreDumpArgs;
use crate::server_client::ServerStoreClient;
use crate::server_runs::ServerRunLookup;
use crate::shared::{absolute_or_current, print_json_pretty};
@ -21,7 +23,7 @@ use crate::user_config::{load_settings_with_storage_dir, storage_dir};
pub(crate) async fn dump_command(
args: &StoreDumpArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
@ -31,7 +33,7 @@ pub(crate) async fn dump_command(
let state = lookup.client().get_run_state(&run_id).await?;
let source = ServerDumpSource::new(lookup.client(), &run_id);
let file_count = export_run_from_source(&source, &state, &args.output).await?;
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"run_id": run_id,
"output_dir": absolute_or_current(&args.output),

View file

@ -2,16 +2,17 @@ pub(crate) mod dump;
pub(crate) mod rebuild;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, StoreCommand, StoreNamespace};
use crate::args::{StoreCommand, StoreNamespace};
pub(crate) async fn dispatch(
ns: StoreNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
match ns.command {
StoreCommand::Dump(args) => dump::dump_command(&args, globals, printer).await,
StoreCommand::Dump(args) => dump::dump_command(&args, cli, printer).await,
}
}

View file

@ -3,19 +3,22 @@ use chrono::{DateTime, Utc};
use cli_table::format::{Border, Justify, Separator};
use cli_table::{Cell, CellStruct, Style, Table};
use fabro_api::types;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::{DfArgs, GlobalArgs};
use crate::args::DfArgs;
use crate::command_context::CommandContext;
use crate::server_client;
use crate::shared::{format_size, print_json_pretty};
pub(super) async fn df_command(
args: &DfArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
let output = server
.api()
@ -26,7 +29,8 @@ pub(super) async fn df_command(
.map_err(server_client::map_api_error)?
.into_inner();
let storage_dir = if globals.json {
let json = cli.output.format == OutputFormat::Json;
let storage_dir = if json {
None
} else {
server
@ -39,14 +43,14 @@ pub(super) async fn df_command(
.storage_dir
};
df_from(&output, storage_dir.as_deref(), globals)
df_from(&output, storage_dir.as_deref(), json)
}
#[allow(clippy::print_stdout)]
fn df_from(
output: &types::DiskUsageResponse,
storage_dir: Option<&str>,
globals: &GlobalArgs,
json_output: bool,
) -> Result<()> {
let runs_summary = output
.summary
@ -82,7 +86,7 @@ fn df_from(
};
let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 };
if globals.json {
if json_output {
print_json_pretty(output)?;
return Ok(());
}

View file

@ -1,17 +1,20 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use futures::StreamExt;
use crate::args::{GlobalArgs, SystemEventsArgs};
use crate::args::SystemEventsArgs;
use crate::command_context::CommandContext;
use crate::{server_client, sse};
pub(super) async fn events_command(
args: &SystemEventsArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
let mut request = server.api().attach_events();
@ -23,16 +26,17 @@ pub(super) async fn events_command(
let mut stream = response.into_inner();
let mut pending = Vec::new();
let json = cli.output.format == OutputFormat::Json;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow::anyhow!("{err}"))?;
pending.extend_from_slice(&chunk);
for payload in sse::drain_sse_payloads(&mut pending, false) {
render_sse_payload(&payload, globals.json)?;
render_sse_payload(&payload, json)?;
}
}
for payload in sse::drain_sse_payloads(&mut pending, true) {
render_sse_payload(&payload, globals.json)?;
render_sse_payload(&payload, json)?;
}
Ok(())

View file

@ -1,17 +1,20 @@
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, SystemInfoArgs};
use crate::args::SystemInfoArgs;
use crate::command_context::CommandContext;
use crate::server_client;
use crate::shared::print_json_pretty;
pub(super) async fn info_command(
args: &SystemInfoArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
let response = server
.api()
@ -21,7 +24,7 @@ pub(super) async fn info_command(
.map_err(server_client::map_api_error)?
.into_inner();
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&response)?;
return Ok(());
}

View file

@ -4,20 +4,23 @@ mod info;
mod prune;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::CliLayer;
use fabro_util::printer::Printer;
pub(crate) use prune::parse_duration;
use crate::args::{GlobalArgs, SystemCommand, SystemNamespace};
use crate::args::{SystemCommand, SystemNamespace};
pub(crate) async fn dispatch(
ns: SystemNamespace,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
match ns.command {
SystemCommand::Info(args) => info::info_command(&args, globals, printer).await,
SystemCommand::Prune(args) => prune::prune_command(&args, globals, printer).await,
SystemCommand::Df(args) => df::df_command(&args, globals, printer).await,
SystemCommand::Events(args) => events::events_command(&args, globals, printer).await,
SystemCommand::Info(args) => info::info_command(&args, cli, cli_layer, printer).await,
SystemCommand::Prune(args) => prune::prune_command(&args, cli, cli_layer, printer).await,
SystemCommand::Df(args) => df::df_command(&args, cli, cli_layer, printer).await,
SystemCommand::Events(args) => events::events_command(&args, cli, cli_layer, printer).await,
}
}

View file

@ -2,20 +2,23 @@ use std::collections::HashMap;
use anyhow::{Context, Result, bail};
use fabro_api::types;
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_util::printer::Printer;
use tracing::{debug, info};
use crate::args::{GlobalArgs, RunsPruneArgs};
use crate::args::RunsPruneArgs;
use crate::command_context::CommandContext;
use crate::server_client;
use crate::shared::{format_size, print_json_pretty};
pub(super) async fn prune_command(
args: &RunsPruneArgs,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> Result<()> {
let ctx = CommandContext::for_connection(&args.connection, printer)?;
let ctx = CommandContext::for_connection(&args.connection, printer, cli.clone(), cli_layer)?;
let server = ctx.server().await?;
let response = server
.api()
@ -32,7 +35,7 @@ pub(super) async fn prune_command(
.await
.map_err(server_client::map_api_error)?
.into_inner();
prune_from(&response, globals, printer)
prune_from(&response, cli.output.format == OutputFormat::Json, printer)
}
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
@ -53,7 +56,7 @@ pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
fn prune_from(
response: &types::PruneRunsResponse,
globals: &GlobalArgs,
json_output: bool,
printer: Printer,
) -> Result<()> {
let total_count = response.total_count.unwrap_or_default();
@ -66,7 +69,7 @@ fn prune_from(
"pruning runs"
);
if globals.json {
if json_output {
print_json_pretty(response)?;
return Ok(());
}

View file

@ -4,12 +4,14 @@ use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::Home;
use fabro_util::printer::Printer;
use serde::Serialize;
use tracing::warn;
use crate::args::{GlobalArgs, UninstallArgs};
use crate::args::UninstallArgs;
use crate::commands::server;
use crate::shared::{format_size, print_json_pretty, tilde_path};
use crate::user_config;
@ -29,14 +31,15 @@ struct Inventory {
#[allow(clippy::unused_async)] // call site requires async
pub(crate) async fn run_uninstall(
args: &UninstallArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let json = cli.output.format == OutputFormat::Json;
let home = Home::from_env();
let home_root = home.root().to_path_buf();
if !looks_like_fabro_home(&home_root) {
if globals.json {
if json {
print_json_pretty(&serde_json::json!({ "status": "not_installed" }))?;
} else {
fabro_util::printerr!(printer, "Fabro is not installed.");
@ -52,7 +55,7 @@ pub(crate) async fn run_uninstall(
let inventory = build_inventory(&home_root, &storage_dir);
if !args.yes {
if globals.json {
if json {
print_json_pretty(&inventory)?;
} else {
print_preview(&inventory, printer);
@ -60,7 +63,7 @@ pub(crate) async fn run_uninstall(
return Ok(());
}
execute_uninstall(&inventory, globals.json, printer).await
execute_uninstall(&inventory, json, printer).await
}
fn build_inventory(home_root: &Path, storage_dir: &Path) -> Inventory {

View file

@ -3,6 +3,8 @@ use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use semver::Version;
use sha2::{Digest, Sha256};
@ -10,7 +12,7 @@ use tokio::process::Command as TokioCommand;
use tokio::task::JoinHandle;
use tracing::debug;
use crate::args::{GlobalArgs, UpgradeArgs};
use crate::args::UpgradeArgs;
use crate::shared::print_json_pretty;
// ── Download backend abstraction ───────────────────────────────────────────
@ -226,7 +228,7 @@ impl UpgradeCheckState {
pub(crate) async fn run_upgrade(
args: UpgradeArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let backend = select_backend().await;
@ -268,7 +270,7 @@ pub(crate) async fn run_upgrade(
}
}
std::cmp::Ordering::Equal if !args.force => {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": current.to_string(),
@ -282,7 +284,7 @@ pub(crate) async fn run_upgrade(
}
if args.dry_run {
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": target.to_string(),
@ -358,7 +360,7 @@ pub(crate) async fn run_upgrade(
let _ = fs::set_permissions(&current_exe, fs::Permissions::from_mode(0o755));
}
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"previous_version": current.to_string(),
"installed_version": target.to_string(),
@ -374,12 +376,8 @@ pub(crate) async fn run_upgrade(
/// Spawn a background task that checks for a newer version and prints a notice
/// to stderr after the main command completes. Returns a handle that should be
/// awaited at the end of `main_inner`.
pub(crate) fn spawn_upgrade_check(
no_upgrade_check: bool,
upgrade_check_enabled: bool,
printer: Printer,
) -> Option<JoinHandle<()>> {
if no_upgrade_check || !upgrade_check_enabled {
pub(crate) fn spawn_upgrade_check(check: bool, printer: Printer) -> Option<JoinHandle<()>> {
if !check {
return None;
}
Some(tokio::spawn(async move {

View file

@ -1,11 +1,12 @@
use anyhow::bail;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::{CliLayer, OutputFormat};
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, ValidateArgs};
use crate::args::ValidateArgs;
use crate::command_context::CommandContext;
use crate::commands::run::output::api_diagnostics_to_local;
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
@ -14,10 +15,11 @@ use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
pub(crate) async fn run(
args: &ValidateArgs,
styles: &Styles,
globals: &GlobalArgs,
cli: &CliSettings,
cli_layer: &CliLayer,
printer: Printer,
) -> anyhow::Result<()> {
let ctx = CommandContext::for_target(&args.target, printer)?;
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
let built = build_run_manifest(ManifestBuildInput {
workflow: args.workflow.clone(),
cwd: ctx.cwd().to_path_buf(),
@ -31,7 +33,7 @@ pub(crate) async fn run(
let response = client.run_preflight(built.manifest).await?;
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"workflow_name": response.workflow.name,
"nodes": response.workflow.nodes,

View file

@ -2,14 +2,16 @@ use std::path::Path;
use anyhow::{Context, Result, bail};
use fabro_config::project::{discover_project_config, resolve_fabro_root};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, WorkflowCreateArgs};
use crate::args::WorkflowCreateArgs;
use crate::shared::{print_json_pretty, relative_path};
pub(super) fn create_command(
args: &WorkflowCreateArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let cwd = std::env::current_dir()?;
@ -24,7 +26,7 @@ pub(super) fn create_command(
let fabro_root = resolve_fabro_root(&config_path, &config);
let created = write_workflow_scaffold(args, &fabro_root)?;
if globals.json {
if cli.output.format == OutputFormat::Json {
let created: Vec<_> = created.iter().map(|path| relative_path(path)).collect();
print_json_pretty(&serde_json::json!({
"name": args.name,

View file

@ -5,17 +5,19 @@ use fabro_config::project::{
WorkflowInfo, WorkflowSource, discover_project_config, list_workflows_detailed,
resolve_fabro_root,
};
use fabro_types::settings::CliSettings;
use fabro_types::settings::cli::OutputFormat;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, WorkflowListArgs};
use crate::args::WorkflowListArgs;
use crate::shared::{color_if, print_json_pretty, relative_path};
const GOAL_MAX_LEN: usize = 60;
pub(super) fn list_command(
_args: &WorkflowListArgs,
globals: &GlobalArgs,
cli: &CliSettings,
printer: Printer,
) -> Result<()> {
let styles = Styles::detect_stderr();
@ -34,7 +36,7 @@ pub(super) fn list_command(
let workflows = list_workflows_detailed(Some(&project_wf_dir), user_wf_dir.as_deref());
if globals.json {
if cli.output.format == OutputFormat::Json {
print_json_pretty(&workflows)?;
return Ok(());
}

View file

@ -2,17 +2,14 @@ mod create;
mod list;
use anyhow::Result;
use fabro_types::settings::CliSettings;
use fabro_util::printer::Printer;
use crate::args::{GlobalArgs, WorkflowCommand, WorkflowNamespace};
use crate::args::{WorkflowCommand, WorkflowNamespace};
pub(crate) fn dispatch(
ns: WorkflowNamespace,
globals: &GlobalArgs,
printer: Printer,
) -> Result<()> {
pub(crate) fn dispatch(ns: WorkflowNamespace, cli: &CliSettings, printer: Printer) -> Result<()> {
match ns.command {
WorkflowCommand::List(args) => list::list_command(&args, globals, printer),
WorkflowCommand::Create(args) => create::create_command(&args, globals, printer),
WorkflowCommand::List(args) => list::list_command(&args, cli, printer),
WorkflowCommand::Create(args) => create::create_command(&args, cli, printer),
}
}

View file

@ -18,10 +18,15 @@ mod user_config;
use std::ffi::OsString;
use anyhow::Result;
use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace};
use args::{
Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace,
global_args_cli_layer, printer_from_verbosity, require_no_json_override,
};
use clap::{CommandFactory, Parser};
use fabro_config::merge::combine_files;
use fabro_config::user::load_settings_config;
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -120,43 +125,45 @@ async fn main_inner() -> (String, Result<()>) {
let cli = Cli::parse();
let Cli { globals, command } = cli;
let printer = Printer::from_flags(globals.quiet, globals.verbose);
let bootstrap_printer = Printer::from_flags(globals.quiet, globals.verbose);
let cli_layer = global_args_cli_layer(&globals);
let process_local_json = globals.json;
let command_name = command.name().to_string();
let (config_log_level, upgrade_check_enabled) = {
if let Commands::Server(ServerNamespace {
command:
ServerCommand::Start(args::ServerStartArgs {
serve_args: args, ..
})
| ServerCommand::Serve(args::ServerServeArgs {
serve_args: args, ..
}),
}) = command.as_ref()
{
match load_settings_config(args.config.as_deref()) {
Ok(layer) => {
let server_settings = layer;
(
server_settings
.server
.as_ref()
.and_then(|server| server.logging.as_ref())
.and_then(|logging| logging.level.clone()),
false,
)
}
Err(err) => return (command_name, Err(err.into())),
}
} else {
match user_config::load_settings() {
Ok(cli_settings) => match user_config::resolve_cli_settings(&cli_settings) {
Ok(resolved_cli) => (resolved_cli.logging.level, resolved_cli.updates.check),
Err(err) => return (command_name, Err(err)),
},
Err(err) => return (command_name, Err(err)),
}
let user_settings = match user_config::load_settings() {
Ok(settings) => settings,
Err(err) => return (command_name, Err(err)),
};
let combined_settings = combine_files(user_settings, SettingsLayer {
cli: Some(cli_layer.clone()),
..SettingsLayer::default()
});
let cli_settings = match user_config::resolve_cli_settings(&combined_settings) {
Ok(cli_settings) => cli_settings,
Err(err) => return (command_name, Err(err)),
};
let printer = printer_from_verbosity(cli_settings.output.verbosity);
let config_log_level = if let Commands::Server(ServerNamespace {
command:
ServerCommand::Start(args::ServerStartArgs {
serve_args: args, ..
})
| ServerCommand::Serve(args::ServerServeArgs {
serve_args: args, ..
}),
}) = command.as_ref()
{
match load_settings_config(args.config.as_deref()) {
Ok(layer) => layer
.server
.as_ref()
.and_then(|server| server.logging.as_ref())
.and_then(|logging| logging.level.clone()),
Err(err) => return (command_name, Err(err.into())),
}
} else {
cli_settings.logging.level.clone()
};
let log_prefix = if command_name == "server start" || command_name == "server __serve" {
@ -166,7 +173,10 @@ async fn main_inner() -> (String, Result<()>) {
};
if let Err(err) = logging::init_tracing(globals.debug, config_log_level.as_deref(), log_prefix)
{
fabro_util::printerr!(printer, "Warning: failed to initialize logging: {err:#}");
fabro_util::printerr!(
bootstrap_printer,
"Warning: failed to initialize logging: {err:#}"
);
}
debug!(command = %command_name, "CLI command started");
@ -178,59 +188,74 @@ async fn main_inner() -> (String, Result<()>) {
| Commands::Repo(_)
| Commands::Install(_)
) {
commands::upgrade::spawn_upgrade_check(
globals.no_upgrade_check,
upgrade_check_enabled,
printer,
)
commands::upgrade::spawn_upgrade_check(cli_settings.updates.check, printer)
} else {
None
};
let result = Box::pin(async move {
match *command {
Commands::Exec(args) => commands::exec::execute(args, &globals, printer).await?,
Commands::Exec(args) => commands::exec::execute(args, &cli_settings, printer).await?,
Commands::RunCmd(cmd) => {
Box::pin(commands::run::dispatch(cmd, &globals, printer)).await?;
Box::pin(commands::run::dispatch(
cmd,
&cli_settings,
&cli_layer,
process_local_json,
printer,
))
.await?;
}
Commands::Preflight(args) => {
commands::preflight::execute(args, &globals, printer).await?;
commands::preflight::execute(args, &cli_settings, &cli_layer, printer).await?;
}
Commands::Validate(args) => {
let styles = Styles::detect_stderr();
commands::validate::run(&args, &styles, &globals, printer).await?;
commands::validate::run(&args, &styles, &cli_settings, &cli_layer, printer).await?;
}
Commands::Graph(args) => {
let styles = Styles::detect_stderr();
commands::graph::run(&args, &styles, &globals, printer).await?;
commands::graph::run(
&args,
&styles,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
}
Commands::Parse(args) => {
commands::parse::run(&args, &globals, printer)?;
commands::parse::run(&args, &cli_settings, printer)?;
}
Commands::Artifact(ns) => {
commands::artifact::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
}
Commands::Store(ns) => commands::store::dispatch(ns, &cli_settings, printer).await?,
Commands::RunsCmd(cmd) => {
commands::runs::dispatch(cmd, &cli_settings, &cli_layer, printer).await?;
}
Commands::Artifact(ns) => commands::artifact::dispatch(ns, &globals, printer).await?,
Commands::Store(ns) => commands::store::dispatch(ns, &globals, printer).await?,
Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd, &globals, printer).await?,
Commands::Model { command } => {
commands::model::execute(command, &globals, printer).await?;
commands::model::execute(command, &cli_settings, &cli_layer, printer).await?;
}
Commands::Server(ns) => {
Box::pin(commands::server::dispatch(ns.command, &globals, printer)).await?;
}
Commands::Doctor(args) => {
let cli_settings = user_config::load_settings()?;
let verbose = args.verbose
|| user_config::resolve_cli_settings(&cli_settings)?
.output
.verbosity
== OutputVerbosity::Verbose;
let verbose =
args.verbose || cli_settings.output.verbosity == OutputVerbosity::Verbose;
let exit_code = Box::pin(commands::doctor::run_doctor(
&args, verbose, &globals, printer,
&args,
verbose,
&cli_settings,
&cli_layer,
printer,
))
.await?;
std::process::exit(exit_code);
}
Commands::Discord => {
if globals.json {
if process_local_json {
shared::print_json_pretty(&serde_json::json!({
"url": "https://fabro.sh/discord",
}))?;
@ -239,7 +264,7 @@ async fn main_inner() -> (String, Result<()>) {
}
}
Commands::Docs => {
if globals.json {
if process_local_json {
shared::print_json_pretty(&serde_json::json!({
"url": "https://docs.fabro.sh/",
}))?;
@ -247,29 +272,72 @@ async fn main_inner() -> (String, Result<()>) {
open::that("https://docs.fabro.sh/")?;
}
}
Commands::Repo(ns) => commands::repo::dispatch(ns, &globals, printer).await?,
Commands::Repo(ns) => {
commands::repo::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
}
Commands::Install(args) => {
Box::pin(commands::install::run_install(&args, &globals, printer)).await?;
Box::pin(commands::install::run_install(
&args,
&cli_settings,
&cli_layer,
process_local_json,
printer,
))
.await?;
}
Commands::Uninstall(args) => {
commands::uninstall::run_uninstall(&args, &globals, printer).await?;
commands::uninstall::run_uninstall(&args, &cli_settings, printer).await?;
}
Commands::Pr(ns) => {
Box::pin(commands::pr::dispatch(
ns,
&cli_settings,
&cli_layer,
printer,
))
.await?;
}
Commands::Secret(ns) => {
commands::secret::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
}
Commands::Pr(ns) => Box::pin(commands::pr::dispatch(ns, &globals, printer)).await?,
Commands::Secret(ns) => commands::secret::dispatch(ns, &globals, printer).await?,
Commands::Settings(args) => {
Box::pin(commands::config::execute(&args, &globals, printer)).await?;
Box::pin(commands::config::execute(
&args,
&cli_settings,
&cli_layer,
printer,
))
.await?;
}
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &globals, printer)?,
Commands::Workflow(ns) => commands::workflow::dispatch(ns, &cli_settings, printer)?,
Commands::Upgrade(args) => {
commands::upgrade::run_upgrade(args, &globals, printer).await?;
commands::upgrade::run_upgrade(args, &cli_settings, printer).await?;
}
Commands::Provider(ns) => {
commands::provider::dispatch(
ns,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
}
Commands::Provider(ns) => commands::provider::dispatch(ns, &globals, printer).await?,
Commands::Sandbox { command } => {
commands::sandbox::dispatch(command, &globals, printer).await?;
commands::sandbox::dispatch(
command,
&cli_settings,
&cli_layer,
process_local_json,
printer,
)
.await?;
}
Commands::System(ns) => {
commands::system::dispatch(ns, &cli_settings, &cli_layer, printer).await?;
}
Commands::System(ns) => commands::system::dispatch(ns, &globals, printer).await?,
Commands::Completion(args) => {
globals.require_no_json()?;
require_no_json_override(process_local_json)?;
let mut cmd = Cli::command();
let shell = args.shell;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {

View file

@ -409,7 +409,7 @@ fn create_persists_requested_overrides_into_store() {
}
#[test]
fn create_json_implies_auto_approve() {
fn create_json_does_not_imply_auto_approve() {
let context = test_context!();
let workflow = fixture("simple.fabro");
let output = context
@ -442,7 +442,7 @@ fn create_json_implies_auto_approve() {
)
.execution
.approval
== fabro_types::settings::run::ApprovalMode::Auto
!= fabro_types::settings::run::ApprovalMode::Auto
);
}

View file

@ -32,6 +32,62 @@ fn settings_json_outputs_parseable_json() {
assert!(value.is_object());
}
#[test]
fn settings_uses_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let output = context.settings().output().expect("command should run");
assert!(output.status.success());
let value: Value =
serde_json::from_slice(&output.stdout).expect("settings config JSON should parse");
assert!(value.is_object());
}
#[test]
fn secret_list_uses_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let output = context
.command()
.args(["secret", "list"])
.output()
.expect("command should run");
assert!(output.status.success());
let value: Value =
serde_json::from_slice(&output.stdout).expect("secret list config JSON should parse");
assert_eq!(value, Value::Array(vec![]));
}
#[test]
fn completion_succeeds_with_json_output_format_from_home_config() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
);
let output = context
.command()
.args(["completion", "zsh"])
.output()
.expect("command should run");
assert!(output.status.success());
let stdout = output_stdout(&output);
assert!(stdout.contains("#compdef"));
assert!(serde_json::from_slice::<Value>(&output.stdout).is_err());
}
#[test]
fn ps_supports_global_flag_and_env_var() {
let context = test_context!();

View file

@ -835,13 +835,13 @@ fn run_id_passthrough_uses_provided_ulid() {
}
#[test]
fn json_run_implies_auto_approve_for_human_gates() {
fn json_run_requires_manual_input_for_human_gates_without_auto_approve() {
let context = test_context!();
let workflow = context.temp_dir.join("human-gate.fabro");
context.write_temp(
"human-gate.fabro",
r#"digraph HumanGate {
graph [goal="Route through the default approval path"]
graph [goal="Require explicit approval before continuing"]
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
approve [shape=hexagon, label="Approve?"]
@ -870,806 +870,31 @@ fn json_run_implies_auto_approve_for_human_gates() {
.expect("command should execute");
assert!(
output.status.success(),
"command failed:\nstdout:\n{}\nstderr:\n{}",
!output.status.success(),
"command unexpectedly succeeded:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let mut progress: Vec<Value> = String::from_utf8(output.stdout)
let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8");
assert!(
stderr.contains("This run is waiting for human input, but --json is non-interactive."),
"stderr should explain the non-interactive interview failure:\n{stderr}"
);
let progress: Vec<Value> = String::from_utf8(output.stdout)
.expect("stdout should be UTF-8")
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).expect("run JSON output should be JSONL"))
.collect();
for event in &mut progress {
if let Some(properties) = event.get_mut("properties").and_then(Value::as_object_mut) {
if properties.contains_key("manifest_blob") {
properties.insert(
"manifest_blob".to_string(),
Value::String("[BLOB_ID]".to_string()),
);
}
if properties.contains_key("definition_blob") {
properties.insert(
"definition_blob".to_string(),
Value::String("[BLOB_ID]".to_string()),
);
}
}
// Strip fields that vary between runs (version, server stanzas that
// carry machine-specific values, cli.target sockets).
if let Some(settings) = event
.pointer_mut("/properties/settings")
.and_then(Value::as_object_mut)
{
settings.remove("_version");
settings.remove("server");
settings.remove("version");
}
if let Some(target) = event
.pointer_mut("/properties/settings/cli/target")
.and_then(Value::as_object_mut)
{
if target.contains_key("path") {
target.insert(
"path".to_string(),
Value::String("[CLI_SOCKET]".to_string()),
);
}
}
let Some(model) = event.pointer_mut("/properties/settings/run/model") else {
continue;
};
let Some(model) = model.as_object_mut() else {
continue;
};
model.insert("name".to_string(), Value::String("[LLM_MODEL]".to_string()));
model.insert(
"provider".to_string(),
Value::String("[LLM_PROVIDER]".to_string()),
);
}
fabro_json_snapshot!(context, &progress, @r#"
[
{
"actor": {
"display": "dev",
"id": "dev",
"kind": "user"
},
"event": "run.created",
"id": "[EVENT_ID]",
"properties": {
"graph": {
"attrs": {
"goal": {
"String": "Route through the default approval path"
}
},
"edges": [
{
"attrs": {},
"from": "start",
"to": "approve"
},
{
"attrs": {
"label": {
"String": "[A] Approve"
}
},
"from": "approve",
"to": "ship"
},
{
"attrs": {
"label": {
"String": "[R] Revise"
}
},
"from": "approve",
"to": "revise"
},
{
"attrs": {},
"from": "ship",
"to": "exit"
},
{
"attrs": {},
"from": "revise",
"to": "exit"
}
],
"name": "HumanGate",
"nodes": {
"approve": {
"attrs": {
"label": {
"String": "Approve?"
},
"shape": {
"String": "hexagon"
}
},
"id": "approve"
},
"exit": {
"attrs": {
"label": {
"String": "Exit"
},
"shape": {
"String": "Msquare"
}
},
"id": "exit"
},
"revise": {
"attrs": {
"script": {
"String": "echo revised"
},
"shape": {
"String": "parallelogram"
}
},
"id": "revise"
},
"ship": {
"attrs": {
"script": {
"String": "echo shipped"
},
"shape": {
"String": "parallelogram"
}
},
"id": "ship"
},
"start": {
"attrs": {
"label": {
"String": "Start"
},
"shape": {
"String": "Mdiamond"
}
},
"id": "start"
}
}
},
"host_repo_path": "[TEMP_DIR]",
"manifest_blob": "[BLOB_ID]",
"provenance": {
"client": {
"name": "fabro-cli",
"user_agent": "fabro-cli/0.176.2",
"version": "0.176.2"
},
"server": {
"version": "0.176.2"
},
"subject": {
"auth_method": "dev_token",
"login": "dev"
}
},
"run_dir": "[RUN_DIR]",
"settings": {
"workflow": {
"graph": "workflow.fabro"
},
"cli": {
"exec": {
"prevent_idle_sleep": false
},
"output": {
"format": "text",
"verbosity": "normal"
},
"target": {
"path": "[CLI_SOCKET]",
"type": "unix"
},
"updates": {
"check": true
}
},
"features": {
"session_sandboxes": false
},
"project": {
"directory": "."
},
"run": {
"execution": {
"approval": "auto",
"mode": "normal",
"retros": false
},
"goal": "Route through the default approval path",
"model": {
"name": "[LLM_MODEL]",
"provider": "[LLM_PROVIDER]"
},
"prepare": {
"timeout": "5m"
},
"sandbox": {
"devcontainer": false,
"local": {
"worktree_mode": "clean"
},
"preserve": false,
"provider": "local"
}
}
},
"workflow_slug": "human-gate",
"workflow_source": "digraph HumanGate {/n graph [goal=\"Route through the default approval path\"]/n start [shape=Mdiamond, label=\"Start\"]/n exit [shape=Msquare, label=\"Exit\"]/n approve [shape=hexagon, label=\"Approve?\"]/n ship [shape=parallelogram, script=\"echo shipped\"]/n revise [shape=parallelogram, script=\"echo revised\"]/n start -> approve/n approve -> ship [label=\"[A] Approve\"]/n approve -> revise [label=\"[R] Revise\"]/n ship -> exit/n revise -> exit/n}/n",
"working_directory": "[TEMP_DIR]"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "run.submitted",
"id": "[EVENT_ID]",
"properties": {
"definition_blob": "[BLOB_ID]"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "run.starting",
"id": "[EVENT_ID]",
"properties": {
"reason": "sandbox_initializing"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "sandbox.initializing",
"id": "[EVENT_ID]",
"properties": {
"provider": "local"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "sandbox.ready",
"id": "[EVENT_ID]",
"properties": {
"duration_ms": "[DURATION_MS]",
"provider": "local"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "sandbox.initialized",
"id": "[EVENT_ID]",
"properties": {
"provider": "local",
"working_directory": "[TEMP_DIR]"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "run.started",
"id": "[EVENT_ID]",
"properties": {
"goal": "Route through the default approval path",
"name": "HumanGate"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "run.running",
"id": "[EVENT_ID]",
"properties": {},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.started",
"id": "[EVENT_ID]",
"node_id": "start",
"node_label": "Start",
"properties": {
"attempt": 1,
"handler_type": "start",
"index": 0,
"max_attempts": 1
},
"run_id": "[ULID]",
"stage_id": "start@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.completed",
"id": "[EVENT_ID]",
"node_id": "start",
"node_label": "Start",
"properties": {
"attempt": 1,
"context_values": {
"current.preamble": "Goal: Route through the default approval path/n",
"current_node": "start",
"graph.goal": "Route through the default approval path",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.run_id": "[ULID]",
"internal.thread_id": null
},
"duration_ms": "[DURATION_MS]",
"index": 0,
"max_attempts": 1,
"node_visits": {
"start": 1
},
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "start@1",
"ts": "[TIMESTAMP]"
},
{
"event": "edge.selected",
"id": "[EVENT_ID]",
"properties": {
"from_node": "start",
"is_jump": false,
"reason": "unconditional",
"stage_status": "success",
"to_node": "approve"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "checkpoint.completed",
"id": "[EVENT_ID]",
"node_id": "start",
"node_label": "start",
"properties": {
"completed_nodes": [
"start"
],
"context_values": {
"current_node": "start",
"failure_class": "",
"failure_signature": "",
"graph.goal": "Route through the default approval path",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.retry_count.start": 0,
"internal.run_id": "[ULID]",
"internal.thread_id": null,
"outcome": "success"
},
"current_node": "start",
"next_node_id": "approve",
"node_outcomes": {
"start": {
"status": "success",
"usage": null
}
},
"node_visits": {
"start": 1
},
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "start@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.started",
"id": "[EVENT_ID]",
"node_id": "approve",
"node_label": "Approve?",
"properties": {
"attempt": 1,
"handler_type": "human",
"index": 1,
"max_attempts": 1
},
"run_id": "[ULID]",
"stage_id": "approve@1",
"ts": "[TIMESTAMP]"
},
{
"event": "interview.started",
"id": "[EVENT_ID]",
"node_id": "approve",
"node_label": "approve",
"properties": {
"allow_freeform": false,
"options": [
{
"key": "A",
"label": "[A] Approve"
},
{
"key": "R",
"label": "[R] Revise"
}
],
"question": "Approve?",
"question_id": "[ULID]",
"question_type": "multiple_choice",
"stage": "approve"
},
"run_id": "[ULID]",
"stage_id": "approve@1",
"ts": "[TIMESTAMP]"
},
{
"event": "interview.completed",
"id": "[EVENT_ID]",
"node_id": "approve",
"node_label": "approve",
"properties": {
"answer": "A",
"duration_ms": "[DURATION_MS]",
"question": "Approve?",
"question_id": "[ULID]"
},
"run_id": "[ULID]",
"stage_id": "approve@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.completed",
"id": "[EVENT_ID]",
"node_id": "approve",
"node_label": "Approve?",
"properties": {
"attempt": 1,
"context_updates": {
"human.gate.label": "[A] Approve",
"human.gate.selected": "A"
},
"context_values": {
"current.preamble": "Goal: Route through the default approval path/n",
"current_node": "approve",
"failure_class": "",
"failure_signature": "",
"graph.goal": "Route through the default approval path",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.retry_count.start": 0,
"internal.run_id": "[ULID]",
"internal.thread_id": "start",
"outcome": "success",
"thread.start.current_node": "approve"
},
"duration_ms": "[DURATION_MS]",
"index": 1,
"max_attempts": 1,
"node_visits": {
"approve": 1,
"start": 1
},
"preferred_label": "[A] Approve",
"status": "success",
"suggested_next_ids": [
"ship"
]
},
"run_id": "[ULID]",
"stage_id": "approve@1",
"ts": "[TIMESTAMP]"
},
{
"event": "edge.selected",
"id": "[EVENT_ID]",
"properties": {
"from_node": "approve",
"is_jump": false,
"label": "[A] Approve",
"preferred_label": "[A] Approve",
"reason": "preferred_label",
"stage_status": "success",
"suggested_next_ids": [
"ship"
],
"to_node": "ship"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "checkpoint.completed",
"id": "[EVENT_ID]",
"node_id": "approve",
"node_label": "approve",
"properties": {
"completed_nodes": [
"start",
"approve"
],
"context_values": {
"current_node": "approve",
"failure_class": "",
"failure_signature": "",
"graph.goal": "Route through the default approval path",
"human.gate.label": "[A] Approve",
"human.gate.selected": "A",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.retry_count.approve": 0,
"internal.retry_count.start": 0,
"internal.run_id": "[ULID]",
"internal.thread_id": "start",
"outcome": "success",
"preferred_label": "[A] Approve",
"thread.start.current_node": "approve"
},
"current_node": "approve",
"next_node_id": "ship",
"node_outcomes": {
"approve": {
"context_updates": {
"human.gate.label": "[A] Approve",
"human.gate.selected": "A"
},
"preferred_label": "[A] Approve",
"status": "success",
"suggested_next_ids": [
"ship"
],
"usage": null
},
"start": {
"status": "success",
"usage": null
}
},
"node_visits": {
"approve": 1,
"start": 1
},
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "approve@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.started",
"id": "[EVENT_ID]",
"node_id": "ship",
"node_label": "ship",
"properties": {
"attempt": 1,
"handler_type": "command",
"index": 2,
"max_attempts": 1
},
"run_id": "[ULID]",
"stage_id": "ship@1",
"ts": "[TIMESTAMP]"
},
{
"event": "command.started",
"id": "[EVENT_ID]",
"node_id": "ship",
"node_label": "ship",
"properties": {
"command": "echo shipped",
"language": "shell",
"script": "echo shipped"
},
"run_id": "[ULID]",
"stage_id": "ship@1",
"ts": "[TIMESTAMP]"
},
{
"event": "command.completed",
"id": "[EVENT_ID]",
"node_id": "ship",
"node_label": "ship",
"properties": {
"duration_ms": "[DURATION_MS]",
"exit_code": 0,
"stderr": "",
"stdout": "shipped/n",
"timed_out": false
},
"run_id": "[ULID]",
"stage_id": "ship@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.completed",
"id": "[EVENT_ID]",
"node_id": "ship",
"node_label": "ship",
"properties": {
"attempt": 1,
"context_updates": {
"command.output": "shipped/n",
"command.stderr": ""
},
"context_values": {
"current.preamble": "Goal: Route through the default approval path/n/n## Completed stages/n- **approve**: success/n/n## Context/n- human.gate.label: [A] Approve/n- human.gate.selected: A/n",
"current_node": "ship",
"failure_class": "",
"failure_signature": "",
"graph.goal": "Route through the default approval path",
"human.gate.label": "[A] Approve",
"human.gate.selected": "A",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.retry_count.approve": 0,
"internal.retry_count.start": 0,
"internal.run_id": "[ULID]",
"internal.thread_id": "approve",
"outcome": "success",
"preferred_label": "[A] Approve",
"thread.approve.current_node": "ship",
"thread.start.current_node": "approve"
},
"duration_ms": "[DURATION_MS]",
"index": 2,
"max_attempts": 1,
"node_visits": {
"approve": 1,
"ship": 1,
"start": 1
},
"notes": "Script completed: echo shipped",
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "ship@1",
"ts": "[TIMESTAMP]"
},
{
"event": "edge.selected",
"id": "[EVENT_ID]",
"properties": {
"from_node": "ship",
"is_jump": false,
"reason": "unconditional",
"stage_status": "success",
"to_node": "exit"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
},
{
"event": "checkpoint.completed",
"id": "[EVENT_ID]",
"node_id": "ship",
"node_label": "ship",
"properties": {
"completed_nodes": [
"start",
"approve",
"ship"
],
"context_values": {
"command.output": "shipped/n",
"command.stderr": "",
"current_node": "ship",
"failure_class": "",
"failure_signature": "",
"graph.goal": "Route through the default approval path",
"human.gate.label": "[A] Approve",
"human.gate.selected": "A",
"internal.fidelity": "compact",
"internal.node_visit_count": 1,
"internal.retry_count.approve": 0,
"internal.retry_count.ship": 0,
"internal.retry_count.start": 0,
"internal.run_id": "[ULID]",
"internal.thread_id": "approve",
"outcome": "success",
"preferred_label": "[A] Approve",
"thread.approve.current_node": "ship",
"thread.start.current_node": "approve"
},
"current_node": "ship",
"next_node_id": "exit",
"node_outcomes": {
"approve": {
"context_updates": {
"human.gate.label": "[A] Approve",
"human.gate.selected": "A"
},
"preferred_label": "[A] Approve",
"status": "success",
"suggested_next_ids": [
"ship"
],
"usage": null
},
"ship": {
"context_updates": {
"command.output": "shipped/n",
"command.stderr": ""
},
"notes": "Script completed: echo shipped",
"status": "success",
"usage": null
},
"start": {
"status": "success",
"usage": null
}
},
"node_visits": {
"approve": 1,
"ship": 1,
"start": 1
},
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "ship@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.started",
"id": "[EVENT_ID]",
"node_id": "exit",
"node_label": "Exit",
"properties": {
"attempt": 1,
"handler_type": "exit",
"index": 3,
"max_attempts": 1
},
"run_id": "[ULID]",
"stage_id": "exit@1",
"ts": "[TIMESTAMP]"
},
{
"event": "stage.completed",
"id": "[EVENT_ID]",
"node_id": "exit",
"node_label": "Exit",
"properties": {
"attempt": 1,
"duration_ms": "[DURATION_MS]",
"index": 3,
"max_attempts": 1,
"status": "success"
},
"run_id": "[ULID]",
"stage_id": "exit@1",
"ts": "[TIMESTAMP]"
},
{
"event": "run.completed",
"id": "[EVENT_ID]",
"properties": {
"artifact_count": 0,
"duration_ms": "[DURATION_MS]",
"reason": "completed",
"status": "success"
},
"run_id": "[ULID]",
"ts": "[TIMESTAMP]"
}
]
"#);
assert_eq!(
progress[0].pointer("/properties/settings/run/execution/approval"),
Some(&serde_json::json!("auto"))
assert!(
progress
.iter()
.any(|event| event.get("event") == Some(&Value::String("interview.started".into()))),
"stdout should include the interview start event:\n{}",
serde_json::to_string_pretty(&progress).unwrap()
);
}