Infer CLI mode from storage and server flags

This commit is contained in:
Bryan Helmkamp 2026-03-29 20:58:46 -04:00
parent aefb6e01c5
commit 79a366022f
No known key found for this signature in database
51 changed files with 395 additions and 316 deletions

View file

@ -119,13 +119,13 @@ mode = "server"
base_url = "https://fabro.example.com:3000"
```
Or use the `--mode` flag:
Or use the `--server-url` flag:
```bash
fabro --mode server --server-url https://fabro.example.com:3000 models list
fabro --server-url https://fabro.example.com:3000 model list
```
This applies to commands like `fabro models list`, `fabro llm chat`, and `fabro exec`. See [CLI Configuration](/reference/cli-configuration#mode) for the full options including mTLS setup.
This applies to commands like `fabro model list`, `fabro llm chat`, and `fabro exec`. See [CLI Configuration](/reference/cli-configuration#mode) for the full options including mTLS setup.
## Next steps

View file

@ -10,8 +10,8 @@ Fabro now exposes a `POST /completions` endpoint for single-turn LLM completions
The CLI's `fabro llm` commands can now target the server instead of calling providers directly:
```bash
fabro llm prompt "Summarize this file" --mode server
fabro llm chat --mode server
fabro llm prompt "Summarize this file" --server-url http://localhost:3000
fabro llm chat --server-url http://localhost:3000
```
## Two new sandbox providers: exe.dev and Sprites
@ -63,14 +63,14 @@ To migrate, regenerate your TypeScript client and update any direct API calls.
- New `POST /completions` endpoint for single-turn LLM completions with SSE streaming and structured output via JSON Schema
- New `GET /models` endpoint exposes the full LLM model catalog with pagination
- New `POST /models/{id}/test` endpoint for testing model connectivity in server mode
- Session endpoints for interactive LLM chat via `fabro llm chat --mode server`
- Session endpoints for interactive LLM chat via `fabro llm chat --server-url http://localhost:3000`
- Verification API reorganized: `/verifications` split into `/verification/criteria` and `/verification/controls`
</Accordion>
<Accordion title="CLI">
- `fabro llm prompt --mode server` routes prompts through the Fabro server
- `fabro llm chat --mode server` enables interactive chat sessions through the server
- `fabro model list --mode standalone|server` controls whether model list comes from local config or server
- `fabro llm prompt --server-url <url>` routes prompts through the Fabro server
- `fabro llm chat --server-url <url>` enables interactive chat sessions through the server
- `fabro model list --server-url <url>` fetches the model list from the Fabro server
- Added `--goal` arg to `fabro run start` to override the workflow goal from the command line
- Turn and tool-call counts now display correctly in non-TTY mode
</Accordion>

View file

@ -149,18 +149,14 @@ Customize the git author identity used for checkpoint commits. Overrides the ser
## `mode`
Controls whether commands run in-process or delegate to a running Fabro API server.
Controls the default execution mode when neither `--storage-dir` nor `--server-url` is passed.
| Value | Description |
|---|---|
| `"standalone"` | Execute locally (default) |
| `"server"` | Delegate to an Fabro API server |
Override with the `--mode` CLI flag:
```bash
fabro --mode server models list
```
For a single invocation, `--storage-dir` implies standalone mode and `--server-url` implies server mode.
## `[server]` section
@ -170,10 +166,10 @@ Configuration for server mode.
|---|---|---|
| `base_url` | Server URL | `"http://localhost:3000"` |
Override the URL with the `--server-url` CLI flag:
Passing `--server-url` implies server mode and overrides `server.base_url`:
```bash
fabro --mode server --server-url https://fabro.example.com:3000 models list
fabro --server-url https://fabro.example.com:3000 model list
```
### `[server.tls]` section

View file

@ -11,6 +11,8 @@ These flags apply to all subcommands:
|---|---|
| `--debug` | Enable DEBUG-level logging (default is INFO) |
| `--no-upgrade-check` | Skip the automatic background upgrade check |
| `--storage-dir <DIR>` | Storage directory for local run data (default: `~/.fabro`). Implies standalone mode. |
| `--server-url <URL>` | Fabro API server URL (overrides `server.base_url` from `cli.toml`). Implies server mode. |
| `-h, --help` | Print help |
| `-V, --version` | Print version |
@ -70,7 +72,6 @@ fabro run run.toml
| Argument / Flag | Description |
|---|---|
| `<WORKFLOW>` | Path to a `.fabro` workflow file, `.toml` task config, or workflow name (resolved from `fabro/workflows/` in the project, then `~/.fabro/workflows/`). |
| `--storage-dir <DIR>` | Storage directory (default: `~/.fabro`) |
| `--dry-run` | Execute with a simulated LLM backend |
| `--auto-approve` | Auto-approve all human gates |
| `--model <MODEL>` | Override default LLM model |
@ -249,8 +250,6 @@ fabro exec "Refactor the auth module" --permissions full --auto-approve
| `--verbose` | Print full LLM request/response JSON to stderr | — |
| `--skills-dir <DIR>` | Directory containing skill files (overrides default discovery) | — |
| `--output-format <FORMAT>` | Output format: `text` (human-readable) or `json` (NDJSON event stream) | `text` |
| `--mode <MODE>` | `standalone` (default) or `server` — in server mode, routes through the Fabro API's `/completions` endpoint | `standalone` |
| `--server-url <URL>` | Fabro API server URL (overrides `server.base_url` from `cli.toml`) | — |
Permission levels control which tools are auto-approved: `read-only` allows read tools (`read_file`, `grep`, `glob`, `list_dir`) and subagent tools; `read-write` adds write tools (`write_file`, `edit_file`, `apply_patch`); `full` allows all tools including shell commands. Tools outside the permission level are either interactively prompted (if a TTY is present) or denied (with `--auto-approve`). See [default models by provider](/core-concepts/models#default-models).
@ -284,14 +283,13 @@ Start an interactive multi-turn chat session. In server mode, the session is bac
```bash
fabro llm chat
fabro llm chat -m claude-opus-4-6 -s "You are a helpful coding assistant"
fabro llm chat --mode server
fabro llm chat --server-url http://localhost:3000
```
| Flag | Description |
|---|---|
| `-m, --model <MODEL>` | Model to use |
| `-s, --system <SYSTEM>` | System prompt |
| `--mode <MODE>` | `standalone` (default) or `server` — in server mode, routes through the Fabro API |
---
@ -303,14 +301,13 @@ List available LLM models from the built-in catalog. Running `fabro model` with
fabro model list
fabro model list -p anthropic
fabro model list -q sonnet
fabro model list --mode server
fabro model list --server-url http://localhost:3000
```
| Flag | Description |
|---|---|
| `-p, --provider <PROVIDER>` | Filter by provider |
| `-q, --query <QUERY>` | Search for models matching this string (checks ID, display name, and aliases) |
| `--mode <MODE>` | `standalone` (default) or `server` — in server mode, fetches the catalog from the Fabro API |
## `fabro model test`

View file

@ -13,7 +13,7 @@ Each `fabro run` invocation creates a timestamped directory under `~/.fabro/runs
~/.fabro/runs/20260307-01JQXYZ123ABC456DEF789/
```
The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the base storage directory with `--storage-dir` (the runs directory will be `<storage-dir>/runs/`).
The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the base storage directory with the global `--storage-dir` flag (the runs directory will be `<storage-dir>/runs/`).
## Root-level files

View file

@ -6,9 +6,6 @@ use fabro_agent::cli::AgentArgs;
use fabro_graphviz::render::GraphFormat;
use fabro_llm::cli::{ChatArgs, ModelsCommand, PromptArgs};
#[cfg(feature = "server")]
use crate::cli_config;
pub(crate) const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
@ -36,26 +33,14 @@ pub(crate) struct GlobalArgs {
#[arg(long, global = true, conflicts_with = "quiet")]
pub verbose: bool,
/// Execution mode: standalone (in-process) or server (delegate to API)
#[cfg(feature = "server")]
#[arg(long, global = true, value_parser = parse_execution_mode)]
pub mode: Option<cli_config::ExecutionMode>,
/// Server URL (overrides server.base_url from cli.toml)
#[cfg(feature = "server")]
/// Storage directory (default: ~/.fabro)
#[arg(long, global = true)]
pub server_url: Option<String>,
}
pub storage_dir: Option<PathBuf>,
#[cfg(feature = "server")]
pub(crate) fn parse_execution_mode(s: &str) -> Result<cli_config::ExecutionMode, String> {
match s {
"standalone" => Ok(cli_config::ExecutionMode::Standalone),
"server" => Ok(cli_config::ExecutionMode::Server),
_ => Err(format!(
"invalid mode '{s}', expected 'standalone' or 'server'"
)),
}
#[cfg(feature = "server")]
/// Server URL (overrides server.base_url from cli.toml)
#[arg(long, global = true, conflicts_with = "storage_dir")]
pub server_url: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
@ -102,10 +87,6 @@ pub(crate) struct RunArgs {
#[arg(required = true)]
pub(crate) workflow: Option<PathBuf>,
/// Storage directory (default: ~/.fabro)
#[arg(long)]
pub(crate) storage_dir: Option<PathBuf>,
/// Execute with simulated LLM backend
#[arg(long)]
pub(crate) dry_run: bool,

View file

@ -1,9 +1,14 @@
#[cfg(feature = "server")]
use std::path::Path;
#[allow(unused_imports)]
pub(crate) use fabro_config::cli::*;
use fabro_config::ConfigLayer;
use fabro_config::FabroSettings;
use crate::args::GlobalArgs;
#[cfg(feature = "server")]
use tracing::debug;
@ -11,9 +16,19 @@ pub(crate) fn load_cli_settings() -> anyhow::Result<FabroSettings> {
ConfigLayer::cli()?.resolve()
}
pub(crate) fn load_cli_settings_with_globals(
globals: &GlobalArgs,
) -> anyhow::Result<FabroSettings> {
let mut layer = ConfigLayer::cli()?;
if let Some(dir) = &globals.storage_dir {
layer.storage_dir = Some(dir.clone());
}
layer.resolve()
}
#[cfg(feature = "server")]
#[derive(Debug, PartialEq)]
pub struct ResolvedMode {
pub(crate) struct ResolvedMode {
pub mode: ExecutionMode,
pub server_base_url: String,
pub tls: Option<ClientTlsSettings>,
@ -23,14 +38,18 @@ pub struct ResolvedMode {
const DEFAULT_SERVER_URL: &str = "http://localhost:3000";
#[cfg(feature = "server")]
pub fn resolve_mode(
cli_mode: Option<ExecutionMode>,
pub(crate) fn resolve_mode(
cli_storage_dir: Option<&Path>,
cli_server_url: Option<&str>,
settings: &FabroSettings,
) -> ResolvedMode {
let mode = cli_mode
.or_else(|| settings.mode.clone())
.unwrap_or_default();
let mode = if cli_server_url.is_some() {
ExecutionMode::Server
} else if cli_storage_dir.is_some() {
ExecutionMode::Standalone
} else {
settings.mode.clone().unwrap_or_default()
};
let server_defaults = settings.server.as_ref();
@ -51,7 +70,9 @@ pub fn resolve_mode(
}
#[cfg(feature = "server")]
pub fn build_server_client(tls: Option<&ClientTlsSettings>) -> anyhow::Result<reqwest::Client> {
pub(crate) fn build_server_client(
tls: Option<&ClientTlsSettings>,
) -> anyhow::Result<reqwest::Client> {
let Some(tls) = tls else {
return Ok(reqwest::Client::new());
};
@ -82,7 +103,7 @@ pub fn build_server_client(tls: Option<&ClientTlsSettings>) -> anyhow::Result<re
#[cfg(all(test, feature = "server"))]
mod tests {
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use super::*;
@ -97,6 +118,31 @@ mod tests {
assert_eq!(resolved.tls, None);
}
#[test]
fn resolve_mode_storage_dir_forces_standalone() {
let settings = FabroSettings {
mode: Some(ExecutionMode::Server),
..FabroSettings::default()
};
let resolved = resolve_mode(Some(Path::new("/tmp/fabro")), None, &settings);
assert_eq!(resolved.mode, ExecutionMode::Standalone);
}
#[test]
fn resolve_mode_server_url_forces_server() {
let settings = FabroSettings {
mode: Some(ExecutionMode::Standalone),
server: Some(ServerSettings {
base_url: Some("https://config.example.com".to_string()),
tls: None,
}),
..FabroSettings::default()
};
let resolved = resolve_mode(None, Some("https://cli.example.com"), &settings);
assert_eq!(resolved.mode, ExecutionMode::Server);
assert_eq!(resolved.server_base_url, "https://cli.example.com");
}
#[test]
fn resolve_mode_config_overrides_default() {
let settings = FabroSettings {
@ -112,25 +158,6 @@ mod tests {
assert_eq!(resolved.server_base_url, "https://config.example.com");
}
#[test]
fn resolve_mode_cli_overrides_config() {
let settings = FabroSettings {
mode: Some(ExecutionMode::Standalone),
server: Some(ServerSettings {
base_url: Some("https://config.example.com".to_string()),
tls: None,
}),
..FabroSettings::default()
};
let resolved = resolve_mode(
Some(ExecutionMode::Server),
Some("https://cli.example.com"),
&settings,
);
assert_eq!(resolved.mode, ExecutionMode::Server);
assert_eq!(resolved.server_base_url, "https://cli.example.com");
}
#[test]
fn resolve_mode_cli_url_overrides_config_url() {
let settings = FabroSettings {

View file

@ -6,12 +6,12 @@ use fabro_store::RuntimeState;
use fabro_workflows::assets::{AssetEntry, scan_assets};
use fabro_workflows::run_lookup::{resolve_run, runs_base};
use crate::args::AssetCpArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{AssetCpArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::split_run_path;
pub(super) fn cp_command(args: &AssetCpArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(super) fn cp_command(args: &AssetCpArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let (run_id, asset_path) = parse_source(&args.source);
let run = resolve_run(&base, run_id)?;

View file

@ -4,12 +4,12 @@ use fabro_store::RuntimeState;
use fabro_workflows::assets::scan_assets;
use fabro_workflows::run_lookup::{resolve_run, runs_base};
use crate::args::AssetListArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{AssetListArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
pub(super) fn list_command(args: &AssetListArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(super) fn list_command(args: &AssetListArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let run = resolve_run(&base, &args.run_id)?;
let runtime_state = RuntimeState::new(&run.path);

View file

@ -3,11 +3,11 @@ mod list;
use anyhow::Result;
use crate::args::{AssetCommand, AssetNamespace};
use crate::args::{AssetCommand, AssetNamespace, GlobalArgs};
pub(crate) fn dispatch(ns: AssetNamespace) -> Result<()> {
pub(crate) fn dispatch(ns: AssetNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
AssetCommand::List(args) => list::list_command(&args),
AssetCommand::Cp(args) => cp::cp_command(&args),
AssetCommand::List(args) => list::list_command(&args, globals),
AssetCommand::Cp(args) => cp::cp_command(&args, globals),
}
}

View file

@ -1,27 +1,31 @@
use std::io::Write;
use std::path::Path;
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs};
use crate::args::{ConfigCommand, ConfigNamespace, ConfigShowArgs, GlobalArgs};
use fabro_config::{ConfigLayer, FabroSettings};
pub(crate) fn dispatch(ns: ConfigNamespace) -> anyhow::Result<()> {
pub(crate) fn dispatch(ns: ConfigNamespace, globals: &GlobalArgs) -> anyhow::Result<()> {
match ns.command {
ConfigCommand::Show(args) => show_command(&args),
ConfigCommand::Show(args) => show_command(&args, globals),
}
}
fn merged_config(workflow: Option<&Path>) -> anyhow::Result<FabroSettings> {
fn merged_config(workflow: Option<&Path>, globals: &GlobalArgs) -> anyhow::Result<FabroSettings> {
let cwd = std::env::current_dir()?;
let base = match workflow {
Some(path) => ConfigLayer::for_workflow(path, &cwd)?,
None => ConfigLayer::project(&cwd)?,
};
let mut cli = ConfigLayer::cli()?;
if let Some(dir) = &globals.storage_dir {
cli.storage_dir = Some(dir.clone());
}
base.combine(ConfigLayer::cli()?).resolve()
base.combine(cli).resolve()
}
pub(crate) fn show_command(args: &ConfigShowArgs) -> anyhow::Result<()> {
let config = merged_config(args.workflow.as_deref())?;
pub(crate) fn show_command(args: &ConfigShowArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
let config = merged_config(args.workflow.as_deref(), globals)?;
let mut yaml = serde_yaml::to_string(&config)?;
if !yaml.ends_with('\n') {
yaml.push('\n');

View file

@ -9,7 +9,7 @@ use crate::args::GlobalArgs;
use crate::cli_config;
pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = cli_config::load_cli_settings()?;
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled());
let exec_defaults = cli_settings.exec.as_ref();
@ -21,7 +21,7 @@ pub(crate) async fn execute(mut args: AgentArgs, globals: &GlobalArgs) -> Result
);
#[cfg(feature = "server")]
let resolved = cli_config::resolve_mode(
globals.mode.clone(),
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
&cli_settings,
);

View file

@ -19,7 +19,7 @@ pub(super) async fn execute(
#[cfg(feature = "server")]
{
let resolved = crate::cli_config::resolve_mode(
globals.mode.clone(),
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
cli_settings,
);

View file

@ -4,10 +4,10 @@ mod prompt;
use anyhow::Result;
use crate::args::{GlobalArgs, LlmCommand, LlmNamespace};
use crate::cli_config::load_cli_settings;
use crate::cli_config::load_cli_settings_with_globals;
pub(crate) async fn dispatch(ns: LlmNamespace, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
match ns.command {
LlmCommand::Prompt(args) => prompt::execute(args, &cli_settings, globals).await,

View file

@ -19,7 +19,7 @@ pub(super) async fn execute(
#[cfg(feature = "server")]
{
let resolved = crate::cli_config::resolve_mode(
globals.mode.clone(),
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
cli_settings,
);

View file

@ -11,9 +11,9 @@ pub(crate) async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs
let server = {
#[cfg(feature = "server")]
{
let cli_settings = cli_config::load_cli_settings()?;
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
let resolved = cli_config::resolve_mode(
globals.mode.clone(),
globals.storage_dir.as_deref(),
globals.server_url.as_deref(),
&cli_settings,
);

View file

@ -5,14 +5,15 @@ use fabro_config::FabroSettingsExt;
use fabro_workflows::run_lookup::runs_base;
use tracing::info;
use crate::args::PrCloseArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrCloseArgs};
use crate::cli_config::load_cli_settings_with_globals;
pub(super) async fn close_command(
args: PrCloseArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
close_from(&base, args, github_app).await
}

View file

@ -12,14 +12,16 @@ use fabro_workflows::records::{
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::PrCreateArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrCreateArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(super) async fn create_command(
args: PrCreateArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
create_from(&base, args, github_app).await
}
@ -30,10 +32,10 @@ async fn create_from(
github_app: Option<fabro_github::GitHubAppCredentials>,
) -> Result<()> {
let storage_dir = base.parent().unwrap_or(base);
let store = crate::store::build_store(storage_dir)?;
let store = store::build_store(storage_dir)?;
let run = resolve_run_combined(store.as_ref(), base, &args.run_id).await?;
let run_dir = run.path.clone();
let run_store = crate::store::open_run_reader(storage_dir, &run.run_id).await?;
let run_store = store::open_run_reader(storage_dir, &run.run_id).await?;
let record = match run_store.as_ref() {
Some(run_store) => run_store

View file

@ -7,16 +7,18 @@ use fabro_workflows::run_lookup::{runs_base, scan_runs_combined};
use futures::future::join_all;
use tracing::info;
use crate::args::PrListArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrListArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(super) async fn list_command(
args: PrListArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
list_from(store.as_ref(), &base, args, github_app).await
}

View file

@ -6,14 +6,15 @@ use tracing::info;
use fabro_workflows::run_lookup::runs_base;
use crate::args::PrMergeArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrMergeArgs};
use crate::cli_config::load_cli_settings_with_globals;
pub(super) async fn merge_command(
args: PrMergeArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
merge_from(&base, args, github_app).await
}

View file

@ -11,20 +11,21 @@ use anyhow::{Context, Result};
use fabro_workflows::pull_request::PullRequestRecord;
use fabro_workflows::run_lookup::resolve_run_combined;
use crate::args::{PrCommand, PrNamespace};
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrCommand, PrNamespace};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::github::build_github_app_credentials;
use crate::store;
pub(crate) async fn dispatch(ns: PrNamespace) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let github_app = build_github_app_credentials(cli_settings.app_id());
match ns.command {
PrCommand::Create(args) => create::create_command(args, github_app).await,
PrCommand::List(args) => list::list_command(args, github_app).await,
PrCommand::View(args) => view::view_command(args, github_app).await,
PrCommand::Merge(args) => merge::merge_command(args, github_app).await,
PrCommand::Close(args) => close::close_command(args, github_app).await,
PrCommand::Create(args) => create::create_command(args, github_app, globals).await,
PrCommand::List(args) => list::list_command(args, github_app, globals).await,
PrCommand::View(args) => view::view_command(args, github_app, globals).await,
PrCommand::Merge(args) => merge::merge_command(args, github_app, globals).await,
PrCommand::Close(args) => close::close_command(args, github_app, globals).await,
}
}
@ -33,7 +34,7 @@ pub(crate) async fn load_pr_record(
run_id: &str,
) -> Result<(PullRequestRecord, PathBuf)> {
let storage_dir = base.parent().unwrap_or(base);
let store = crate::store::build_store(storage_dir)?;
let store = store::build_store(storage_dir)?;
let run_dir = resolve_run_combined(store.as_ref(), base, run_id)
.await?
.path;

View file

@ -6,14 +6,15 @@ use tracing::info;
use fabro_workflows::run_lookup::runs_base;
use crate::args::PrViewArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PrViewArgs};
use crate::cli_config::load_cli_settings_with_globals;
pub(super) async fn view_command(
args: PrViewArgs,
github_app: Option<fabro_github::GitHubAppCredentials>,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
view_from(&base, args, github_app).await
}

View file

@ -16,13 +16,17 @@ use fabro_util::terminal::Styles;
use fabro_workflows::git::{GitSyncStatus, sync_status};
use fabro_workflows::operations::{ValidateInput, WorkflowInput, validate};
use crate::args::PreflightArgs;
use crate::args::{GlobalArgs, PreflightArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::github::build_github_app_credentials;
pub(crate) async fn execute(mut args: PreflightArgs) -> anyhow::Result<()> {
pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = ConfigLayer::cli()?;
let cli_settings: FabroSettings = cli.clone().resolve()?;
let mut cli = ConfigLayer::cli()?;
if let Some(dir) = &globals.storage_dir {
cli.storage_dir = Some(dir.clone());
}
let cli_settings: FabroSettings = load_cli_settings_with_globals(globals)?;
args.verbose = args.verbose || cli_settings.verbose_enabled();
let github_app = build_github_app_credentials(cli_settings.app_id());

View file

@ -16,9 +16,10 @@ use fabro_workflows::outcome::StageStatus;
use fabro_workflows::records::{Conclusion, ConclusionExt, RunRecord, RunRecordExt};
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt};
use tokio::signal::ctrl_c;
use tokio::time::sleep;
use tokio::time::{self, sleep};
use super::run_progress;
use crate::store;
#[cfg(test)]
const ATTACH_STARTUP_GRACE: Duration = Duration::from_millis(200);
@ -44,7 +45,7 @@ pub(crate) async fn attach_run(
.map(|record| record.settings.storage_dir()),
run_id.or_else(|| run_record.as_ref().map(|record| record.run_id.as_str())),
) {
match crate::store::open_run_reader(&storage_dir, run_id).await {
match store::open_run_reader(&storage_dir, run_id).await {
Ok(Some(run_store)) => match run_store.list_events().await {
Ok(events) => {
let event_lines = events
@ -55,7 +56,7 @@ pub(crate) async fn attach_run(
run_dir,
run_store.as_ref(),
event_lines,
events.last().map(|event| event.seq).unwrap_or(0),
events.last().map_or(0, |event| event.seq),
kill_on_detach,
styles,
engine_child,
@ -158,7 +159,7 @@ async fn attach_run_store(
}
let mut saw_event = false;
match tokio::time::timeout(Duration::from_millis(100), stream.next()).await {
match time::timeout(Duration::from_millis(100), stream.next()).await {
Ok(Some(Ok(event))) => {
let line = event_payload_line(&event)?;
progress_ui.handle_json_line(&line);
@ -642,27 +643,24 @@ fn determine_exit_code(conclusion_path: &Path, status_record: Option<RunStatusRe
}
async fn determine_exit_code_with_store(run_store: &dyn RunStore, run_dir: &Path) -> ExitCode {
match run_store.get_conclusion().await {
Ok(Some(conclusion)) => {
let success = matches!(
conclusion.status,
StageStatus::Success | StageStatus::PartialSuccess
);
if success {
ExitCode::from(0)
} else {
ExitCode::from(1)
}
}
Ok(None) | Err(_) => {
let status_path = run_dir.join("status.json");
let conclusion_path = run_dir.join("conclusion.json");
let status_record = match run_store.get_status().await {
Ok(record) => record.or_else(|| read_status_record(&status_path)),
Err(_) => read_status_record(&status_path),
};
determine_exit_code(&conclusion_path, status_record)
if let Ok(Some(conclusion)) = run_store.get_conclusion().await {
let success = matches!(
conclusion.status,
StageStatus::Success | StageStatus::PartialSuccess
);
if success {
ExitCode::from(0)
} else {
ExitCode::from(1)
}
} else {
let status_path = run_dir.join("status.json");
let conclusion_path = run_dir.join("conclusion.json");
let status_record = match run_store.get_status().await {
Ok(record) => record.or_else(|| read_status_record(&status_path)),
Err(_) => read_status_record(&status_path),
};
determine_exit_code(&conclusion_path, status_record)
}
}

View file

@ -3,16 +3,18 @@ use fabro_config::ConfigLayer;
use fabro_util::terminal::Styles;
use crate::args::{GlobalArgs, RunArgs};
use crate::cli_config;
pub(crate) async fn execute(mut args: RunArgs, _globals: &GlobalArgs) -> Result<()> {
pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_settings = cli_config::load_cli_settings_with_globals(globals)?;
let cli = ConfigLayer::cli()?;
let cli_settings = cli.clone().resolve()?;
args.verbose = args.verbose || cli_settings.verbose_enabled();
let quiet = args.detach;
let prevent_idle_sleep = cli_settings.prevent_idle_sleep_enabled();
let (run_id, run_dir) = super::create::create_run(&args, cli, styles, quiet)?;
let (run_id, run_dir) =
super::create::create_run(&args, cli, styles, quiet, globals.storage_dir.clone())?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = crate::sleep_inhibitor::guard(prevent_idle_sleep);

View file

@ -9,8 +9,8 @@ use fabro_workflows::run_lookup::{resolve_run, runs_base};
use tokio::fs;
use tracing::{debug, info};
use crate::args::CpArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{CpArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::split_run_path;
enum CopyDirection {
@ -26,9 +26,9 @@ enum CopyDirection {
},
}
pub(crate) async fn cp_command(args: CpArgs) -> Result<()> {
pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> {
let direction = parse_direction(&args.src, &args.dst)?;
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
match direction {

View file

@ -16,12 +16,16 @@ pub(crate) fn create_run(
cli_defaults: ConfigLayer,
styles: &Styles,
quiet: bool,
storage_dir_override: Option<PathBuf>,
) -> anyhow::Result<(String, PathBuf)> {
let workflow_path = args
.workflow
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--workflow is required"))?;
let cli_args_config = ConfigLayer::try_from(args)?;
let mut cli_args_config = ConfigLayer::try_from(args)?;
if let Some(dir) = storage_dir_override {
cli_args_config.storage_dir = Some(dir);
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let settings: FabroSettings = cli_args_config
.combine(ConfigLayer::for_workflow(workflow_path, &cwd)?)

View file

@ -14,6 +14,7 @@ use fabro_workflows::records::{RunRecord, RunRecordExt};
use crate::cli_config;
use crate::shared;
use crate::store;
pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> {
let _ = fabro_proctitle::init();
@ -31,7 +32,7 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo
fabro_proctitle::set(&format!("fabro: {short_id} {node_id}"));
}) as Arc<dyn Fn(&str) + Send + Sync>
});
let store = crate::store::build_store(&run_record.settings.storage_dir())?;
let store = store::build_store(&run_record.settings.storage_dir())?;
let run_store = open_or_hydrate_run(store.as_ref(), &run_dir).await?;
let github_app = shared::github::build_github_app_credentials(cli_settings.app_id());

View file

@ -10,16 +10,17 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::sandbox_git::GIT_REMOTE;
use tracing::{debug, info};
use crate::args::DiffArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{DiffArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(crate) async fn run(args: DiffArgs) -> Result<()> {
pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> {
info!(run_id = %args.run, "Showing diff");
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
let run_store = crate::store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await?;
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await?;
let patch = resolve_diff(&run.path, run_store.as_deref(), &args).await?;

View file

@ -8,13 +8,13 @@ use fabro_workflows::operations::{
};
use git2::Repository;
use crate::args::ForkArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{ForkArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store::{build_store, open_run_reader};
pub(crate) async fn run(args: &ForkArgs, styles: &Styles) -> Result<()> {
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let durable_store = build_store(&cli_settings.storage_dir())?;
let run_id =
find_run_id_by_prefix_or_store(&repo, durable_store.as_ref(), &args.run_id).await?;

View file

@ -9,15 +9,17 @@ use fabro_store::RunStore;
use fabro_util::terminal::Styles;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use futures::StreamExt;
use tokio::time;
use tracing::{debug, info, warn};
use crate::args::LogsArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, LogsArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(crate) async fn run(args: &LogsArgs, styles: &Styles) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn run(args: &LogsArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
info!(run_id = %run.run_id, "Showing logs");
@ -27,12 +29,12 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles) -> Result<()> {
None => None,
};
let run_store = crate::store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await?;
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await?;
let progress_path = run.path.join("progress.jsonl");
let (all_lines, last_seq, use_store_follow) = match run_store.as_ref() {
Some(run_store) => match run_store.list_events().await {
let (all_lines, last_seq, use_store_follow) = if let Some(run_store) = run_store.as_ref() {
match run_store.list_events().await {
Ok(events) => {
let last_seq = events.last().map(|event| event.seq).unwrap_or(0);
let last_seq = events.last().map_or(0, |event| event.seq);
let lines = events
.iter()
.map(event_payload_line)
@ -50,13 +52,12 @@ pub(crate) async fn run(args: &LogsArgs, styles: &Styles) -> Result<()> {
);
(read_lines(&progress_path)?, 0, false)
}
},
None => {
if !progress_path.exists() {
bail!("No progress.jsonl found for run '{}'", run.run_id);
}
(read_lines(&progress_path)?, 0, false)
}
} else {
if !progress_path.exists() {
bail!("No progress.jsonl found for run '{}'", run.run_id);
}
(read_lines(&progress_path)?, 0, false)
};
let filtered = apply_filters(&all_lines, since_cutoff.as_ref(), args.tail);
@ -252,7 +253,7 @@ async fn follow_store_logs(
let mut out = stdout.lock();
loop {
match tokio::time::timeout(Duration::from_millis(200), stream.next()).await {
match time::timeout(Duration::from_millis(200), stream.next()).await {
Ok(Some(Ok(event))) => {
let line = event_payload_line(&event)?;
if pretty {

View file

@ -5,7 +5,8 @@ use fabro_util::terminal::Styles;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use crate::args::{GlobalArgs, RunCommands};
use crate::cli_config::load_cli_settings;
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(crate) mod attach;
pub(crate) mod command;
@ -36,14 +37,15 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
RunCommands::Create(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli = ConfigLayer::cli()?;
let (run_id, _run_dir) = create::create_run(&args, cli, styles, true)?;
let (run_id, _run_dir) =
create::create_run(&args, cli, styles, true, globals.storage_dir.clone())?;
println!("{run_id}");
Ok(())
}
RunCommands::Start { run } => {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
let child = start::start_run(&run_info.path, false)?;
eprintln!("Started engine process (PID {})", child.id());
@ -51,9 +53,9 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
}
RunCommands::Attach { run } => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &run).await?;
let exit_code =
attach::attach_run(&run_info.path, Some(&run_info.run_id), false, styles, None)
@ -68,34 +70,34 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
launcher_path,
resume,
} => detached::execute(run_dir, launcher_path, resume).await,
RunCommands::Cp(args) => cp::cp_command(args).await,
RunCommands::Preview(args) => preview::run(args).await,
RunCommands::Ssh(args) => ssh::run(args).await,
RunCommands::Diff(args) => diff::run(args).await,
RunCommands::Cp(args) => cp::cp_command(args, globals).await,
RunCommands::Preview(args) => preview::run(args, globals).await,
RunCommands::Ssh(args) => ssh::run(args, globals).await,
RunCommands::Diff(args) => diff::run(args, globals).await,
RunCommands::Logs(args) => {
let styles = Styles::detect_stdout();
logs::run(&args, &styles).await
logs::run(&args, &styles, globals).await
}
RunCommands::Resume(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled())
};
resume::resume_command(args, styles).await
resume::resume_command(args, styles, globals).await
}
RunCommands::Rewind(args) => {
let styles = Styles::detect_stderr();
rewind::run(&args, &styles).await
rewind::run(&args, &styles, globals).await
}
RunCommands::Fork(args) => {
let styles = Styles::detect_stderr();
fork::run(&args, &styles).await
fork::run(&args, &styles, globals).await
}
RunCommands::Wait(args) => {
let styles = Styles::detect_stderr();
wait::run(&args, &styles).await
wait::run(&args, &styles, globals).await
}
}
}

View file

@ -54,7 +54,6 @@ impl TryFrom<&RunArgs> for ConfigLayer {
dry_run: sparse_flag(args.dry_run),
auto_approve: sparse_flag(args.auto_approve),
no_retro: sparse_flag(args.no_retro),
storage_dir: args.storage_dir.clone(),
labels: parse_labels(&args.label),
..Default::default()
})

View file

@ -5,19 +5,18 @@ use fabro_sandbox::daytona::DaytonaSandbox;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::PreviewArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, PreviewArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::validate_daytona_provider;
use crate::store;
pub(crate) async fn run(args: PreviewArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
let sandbox_json = run.path.join("sandbox.json");
let record = match crate::store::open_run_reader(&cli_settings.storage_dir(), &run.run_id)
.await?
{
let record = match store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await? {
Some(run_store) => run_store
.get_sandbox()
.await

View file

@ -4,8 +4,9 @@ use fabro_util::terminal::Styles;
use fabro_workflows::records::{RunRecord, RunRecordExt};
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use crate::args::ResumeArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, ResumeArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
/// Resume an interrupted workflow run.
///
@ -15,10 +16,11 @@ use crate::cli_config::load_cli_settings;
pub(crate) async fn resume_command(
args: ResumeArgs,
styles: &'static Styles,
globals: &GlobalArgs,
) -> anyhow::Result<()> {
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
let run_dir = run.path;

View file

@ -11,14 +11,14 @@ use fabro_workflows::operations::{
};
use git2::Repository;
use crate::args::RewindArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, RewindArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::color_if;
use crate::store::{build_store, open_run_reader};
pub(crate) async fn run(args: &RewindArgs, styles: &Styles) -> Result<()> {
pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let repo = Repository::discover(".").context("not in a git repository")?;
let cli_settings = load_cli_settings()?;
let cli_settings = load_cli_settings_with_globals(globals)?;
let durable_store = build_store(&cli_settings.storage_dir())?;
let run_id =
find_run_id_by_prefix_or_store(&repo, durable_store.as_ref(), &args.run_id).await?;

View file

@ -5,19 +5,18 @@ use fabro_sandbox::daytona::DaytonaSandbox;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use tracing::info;
use crate::args::SshArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, SshArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::validate_daytona_provider;
use crate::store;
pub(crate) async fn run(args: SshArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
let sandbox_json = run.path.join("sandbox.json");
let record = match crate::store::open_run_reader(&cli_settings.storage_dir(), &run.run_id)
.await?
{
let record = match store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await? {
Some(run_store) => run_store
.get_sandbox()
.await

View file

@ -8,20 +8,20 @@ use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, RunStatusRecordExt};
use tracing::info;
use crate::args::WaitArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, WaitArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_duration_ms;
use crate::store;
pub(crate) async fn run(args: &WaitArgs, styles: &Styles) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn run(args: &WaitArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run_info = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
info!(run_id = %run_info.run_id, "Waiting for run to complete");
let run_store =
crate::store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
let run_store = store::open_run_reader(&cli_settings.storage_dir(), &run_info.run_id).await?;
let status_path = run_info.path.join("status.json");
let deadline = args
.timeout

View file

@ -10,8 +10,9 @@ use fabro_workflows::records::{Checkpoint, Conclusion, RunRecord, StartRecord};
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::run_status::RunStatus;
use crate::args::InspectArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, InspectArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
#[derive(Debug, Serialize)]
pub(crate) struct InspectOutput {
@ -25,18 +26,17 @@ pub(crate) struct InspectOutput {
pub sandbox: Option<serde_json::Value>,
}
pub(crate) async fn run(args: &InspectArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn run(args: &InspectArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;
let output =
match crate::store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await? {
Some(run_store) => {
inspect_run_store(&run.run_id, &run.path, run.status, run_store.as_ref()).await
}
None => inspect_run_dir(&run.run_id, &run.path, run.status),
};
let output = match store::open_run_reader(&cli_settings.storage_dir(), &run.run_id).await? {
Some(run_store) => {
inspect_run_store(&run.run_id, &run.path, run.status, run_store.as_ref()).await
}
None => inspect_run_dir(&run.run_id, &run.path, run.status),
};
let json = serde_json::to_string_pretty(&[output])?;
println!("{json}");
Ok(())
@ -55,8 +55,7 @@ async fn inspect_run_store(
status: snapshot
.status
.as_ref()
.map(|record| record.status)
.unwrap_or(status),
.map_or(status, |record| record.status),
run_record: serde_json::to_value(snapshot.run).ok(),
start_record: snapshot
.start

View file

@ -11,16 +11,21 @@ use fabro_util::text::strip_goal_decoration;
use fabro_workflows::run_lookup::{StatusFilter, filter_runs, runs_base, scan_runs_combined};
use fabro_workflows::run_status::RunStatus;
use crate::args::RunsListArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, RunsListArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::{color_if, format_duration_ms, tilde_path};
use crate::store;
use super::short_run_id;
pub(crate) async fn list_command(args: &RunsListArgs, styles: &Styles) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn list_command(
args: &RunsListArgs,
styles: &Styles,
globals: &GlobalArgs,
) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
let runs = scan_runs_combined(store.as_ref(), &base).await?;
let label_filters = parse_label_filters(&args.filter.label);
let filtered = filter_runs(

View file

@ -1,20 +1,20 @@
use anyhow::Result;
use fabro_util::terminal::Styles;
use crate::args::RunsCommands;
use crate::args::{GlobalArgs, RunsCommands};
pub(crate) mod inspect;
pub(crate) mod list;
pub(crate) mod rm;
pub(crate) async fn dispatch(cmd: RunsCommands) -> Result<()> {
pub(crate) async fn dispatch(cmd: RunsCommands, globals: &GlobalArgs) -> Result<()> {
match cmd {
RunsCommands::Ps(args) => {
let styles = Styles::detect_stdout();
list::list_command(&args, &styles).await
list::list_command(&args, &styles, globals).await
}
RunsCommands::Rm(args) => rm::remove_command(&args).await,
RunsCommands::Inspect(args) => inspect::run(&args).await,
RunsCommands::Rm(args) => rm::remove_command(&args, globals).await,
RunsCommands::Inspect(args) => inspect::run(&args, globals).await,
}
}

View file

@ -8,17 +8,18 @@ use tracing::warn;
use fabro_sandbox::reconnect::reconnect as reconnect_sandbox;
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use fabro_workflows::run_status::{RunStatus, write_run_status};
use fabro_workflows::run_status::{RunStatus, RunStatusRecord, write_run_status};
use crate::args::RunsRemoveArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, RunsRemoveArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
use super::short_run_id;
pub(crate) async fn remove_command(args: &RunsRemoveArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn remove_command(args: &RunsRemoveArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
remove_from(args, store.as_ref(), &base).await
}
@ -48,10 +49,7 @@ async fn remove_from(args: &RunsRemoveArgs, store: &dyn Store, base: &Path) -> R
write_run_status(&run.path, RunStatus::Removing, None);
if let Ok(Some(run_store)) = store.open_run_reader(&run.run_id).await {
if let Err(err) = run_store
.put_status(&fabro_workflows::run_status::RunStatusRecord::new(
RunStatus::Removing,
None,
))
.put_status(&RunStatusRecord::new(RunStatus::Removing, None))
.await
{
warn!(

View file

@ -7,12 +7,12 @@ use fabro_store::{NodeVisitRef, RunSnapshot, RunStore};
use fabro_workflows::run_lookup::{resolve_run_combined, runs_base};
use serde::Serialize;
use crate::args::StoreDumpArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, StoreDumpArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::store;
pub(crate) async fn dump_command(args: &StoreDumpArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = store::build_store(&cli_settings.storage_dir())?;
let run = resolve_run_combined(store.as_ref(), &base, &args.run).await?;

View file

@ -2,10 +2,10 @@ mod dump;
use anyhow::Result;
use crate::args::{StoreCommand, StoreNamespace};
use crate::args::{GlobalArgs, StoreCommand, StoreNamespace};
pub(crate) async fn dispatch(ns: StoreNamespace) -> Result<()> {
pub(crate) async fn dispatch(ns: StoreNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
StoreCommand::Dump(args) => dump::dump_command(&args).await,
StoreCommand::Dump(args) => dump::dump_command(&args, globals).await,
}
}

View file

@ -9,16 +9,17 @@ use fabro_config::FabroSettingsExt;
use fabro_workflows::run_lookup::{logs_base, runs_base, scan_runs_combined};
use fabro_workflows::run_status::RunStatus;
use crate::args::DfArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{DfArgs, GlobalArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
use crate::store;
pub(super) async fn df_command(args: &DfArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let data_dir = cli_settings.storage_dir();
let runs_base_dir = runs_base(&data_dir);
let logs_base_dir = logs_base(&data_dir);
let store = crate::store::build_store(&data_dir)?;
let store = store::build_store(&data_dir)?;
df_from(
args,
store.as_ref(),

View file

@ -3,13 +3,13 @@ mod prune;
use anyhow::Result;
use crate::args::{SystemCommand, SystemNamespace};
use crate::args::{GlobalArgs, SystemCommand, SystemNamespace};
pub(crate) use prune::parse_duration;
pub(crate) async fn dispatch(ns: SystemNamespace) -> Result<()> {
pub(crate) async fn dispatch(ns: SystemNamespace, globals: &GlobalArgs) -> Result<()> {
match ns.command {
SystemCommand::Prune(args) => prune::prune_command(&args).await,
SystemCommand::Df(args) => df::df_command(&args).await,
SystemCommand::Prune(args) => prune::prune_command(&args, globals).await,
SystemCommand::Df(args) => df::df_command(&args, globals).await,
}
}

View file

@ -8,14 +8,15 @@ use tracing::{debug, info};
use fabro_workflows::run_lookup::{StatusFilter, filter_runs, runs_base, scan_runs_combined};
use crate::args::RunsPruneArgs;
use crate::cli_config::load_cli_settings;
use crate::args::{GlobalArgs, RunsPruneArgs};
use crate::cli_config::load_cli_settings_with_globals;
use crate::shared::format_size;
use crate::store;
pub(super) async fn prune_command(args: &RunsPruneArgs) -> Result<()> {
let cli_settings = load_cli_settings()?;
pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> {
let cli_settings = load_cli_settings_with_globals(globals)?;
let base = runs_base(&cli_settings.storage_dir());
let store = crate::store::build_store(&cli_settings.storage_dir())?;
let store = store::build_store(&cli_settings.storage_dir())?;
prune_from(args, store.as_ref(), &base).await
}

View file

@ -166,7 +166,7 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Llm(ns) => commands::llm::dispatch(ns, &globals).await?,
Commands::Exec(args) => commands::exec::execute(args, &globals).await?,
Commands::RunCmd(cmd) => commands::run::dispatch(cmd, &globals).await?,
Commands::Preflight(args) => commands::preflight::execute(args).await?,
Commands::Preflight(args) => commands::preflight::execute(args, &globals).await?,
Commands::Validate(args) => {
let styles = Styles::detect_stderr();
commands::validate::run(&args, &styles)?;
@ -178,14 +178,15 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Parse(args) => {
commands::parse::run(&args)?;
}
Commands::Asset(ns) => commands::asset::dispatch(ns)?,
Commands::Store(ns) => commands::store::dispatch(ns).await?,
Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd).await?,
Commands::Asset(ns) => commands::asset::dispatch(ns, &globals)?,
Commands::Store(ns) => commands::store::dispatch(ns, &globals).await?,
Commands::RunsCmd(cmd) => commands::runs::dispatch(cmd, &globals).await?,
Commands::Model { command } => commands::model::execute(command, &globals).await?,
#[cfg(feature = "server")]
Commands::Serve(args) => {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
fabro_server::serve::serve_command(args, styles).await?;
fabro_server::serve::serve_command(args, styles, globals.storage_dir.clone())
.await?;
}
Commands::Doctor { verbose, dry_run } => {
let cli_settings = cli_config::load_cli_settings()?;
@ -207,16 +208,16 @@ async fn main_inner() -> (String, Result<()>) {
Commands::Install { web_url } => {
commands::install::run_install(&web_url).await?;
}
Commands::Pr(ns) => commands::pr::dispatch(ns).await?,
Commands::Pr(ns) => commands::pr::dispatch(ns, &globals).await?,
Commands::Secret(ns) => commands::secret::dispatch(ns)?,
Commands::Config(ns) => commands::config::dispatch(ns)?,
Commands::Config(ns) => commands::config::dispatch(ns, &globals)?,
Commands::Workflow(ns) => commands::workflow::dispatch(ns)?,
Commands::Skill(ns) => commands::skill::dispatch(ns)?,
Commands::Upgrade(args) => {
commands::upgrade::run_upgrade(args).await?;
}
Commands::Provider(ns) => commands::provider::dispatch(ns).await?,
Commands::System(ns) => commands::system::dispatch(ns).await?,
Commands::System(ns) => commands::system::dispatch(ns, &globals).await?,
Commands::SendAnalytics { path } => {
let result = sender::upload(&path).await;
let _ = std::fs::remove_file(&path);
@ -306,6 +307,46 @@ mod tests {
}
}
#[test]
fn parse_global_storage_dir_after_subcommand() {
let cli = Cli::try_parse_from([
"fabro",
"run",
"test/simple.fabro",
"--storage-dir",
"/tmp/fabro",
])
.expect("should parse");
assert_eq!(
cli.globals.storage_dir.as_deref(),
Some(std::path::Path::new("/tmp/fabro"))
);
match *cli.command {
Commands::RunCmd(RunCommands::Run(args)) => {
assert_eq!(
args.workflow.as_deref(),
Some(std::path::Path::new("test/simple.fabro"))
);
}
_ => panic!("unexpected command variant"),
}
}
#[test]
#[cfg(feature = "server")]
fn parse_server_url_conflicts_with_storage_dir() {
let result = Cli::try_parse_from([
"fabro",
"--storage-dir",
"/tmp/fabro",
"--server-url",
"http://localhost:3000",
"model",
"list",
]);
assert!(result.is_err(), "should fail with conflicting global flags");
}
#[test]
fn parse_store_dump_command() {
let cli = Cli::try_parse_from(["fabro", "store", "dump", "ABC123", "-o", "./out"])

View file

@ -1,6 +1,6 @@
---
source: lib/crates/fabro-cli/tests/it/cli.rs
assertion_line: 349
assertion_line: 745
expression: stdout
---
Start the HTTP API server
@ -16,18 +16,22 @@ Options:
Host address to bind to [default: 127.0.0.1]
--no-upgrade-check
Disable automatic upgrade check
--mode <MODE>
Execution mode: standalone (in-process) or server (delegate to API)
--model <MODEL>
Override default LLM model
--quiet
Suppress non-essential output
--provider <PROVIDER>
Override default LLM provider
--server-url <SERVER_URL>
Server URL (overrides server.base_url from cli.toml)
--verbose
Enable verbose output
--dry-run
Execute with simulated LLM backend
--storage-dir <STORAGE_DIR>
Storage directory (default: ~/.fabro)
--sandbox <SANDBOX>
Sandbox for agent tools
--server-url <SERVER_URL>
Server URL (overrides server.base_url from cli.toml)
--max-concurrent-runs <MAX_CONCURRENT_RUNS>
Maximum number of concurrent run executions
--config <CONFIG>

View file

@ -64,7 +64,11 @@ pub struct ServeArgs {
///
/// Returns an error if the server fails to bind or encounters a fatal error.
#[allow(clippy::print_stderr)]
pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::Result<()> {
pub async fn serve_command(
args: ServeArgs,
styles: &'static Styles,
storage_dir_override: Option<PathBuf>,
) -> anyhow::Result<()> {
// Resolve dry-run mode (same pattern as run.rs)
let dry_run_mode = if args.dry_run {
true
@ -91,7 +95,7 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
// Initialize data directory and SQLite database
let config_path = args.config;
let server_settings = load_server_settings(config_path.as_deref())?;
let data_dir = resolve_storage_dir(&server_settings);
let data_dir = storage_dir_override.unwrap_or_else(|| resolve_storage_dir(&server_settings));
// Shared config for live reloading
let shared_settings = Arc::new(RwLock::new(server_settings));

View file

@ -16,7 +16,8 @@ use fabro_llm::types::{
ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest,
Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage,
};
use fabro_retro::retro::{Retro, derive_retro};
use fabro_retro::retro;
use fabro_retro::retro::Retro;
use fabro_store::{InMemoryStore, Store};
use fabro_util::redact::redact_jsonl_line;
use fabro_workflows::error::FabroError;
@ -820,10 +821,10 @@ async fn execute_run(state: Arc<AppState>, run_id: String) {
Ok(events) => fabro_workflows::extract_stage_durations_from_events(&events),
Err(err) => {
tracing::warn!(run_id = %run_id, error = %err, "Failed to load run events from store");
fabro_retro::retro::extract_stage_durations(&run_options.run_dir)
retro::extract_stage_durations(&run_options.run_dir)
}
};
let retro = derive_retro(
let retro = retro::derive_retro(
&run_id,
"workflow",
"",