mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-29 06:36:51 +00:00
425 lines
15 KiB
Rust
425 lines
15 KiB
Rust
mod args;
|
|
mod cli_config;
|
|
mod commands;
|
|
mod logging;
|
|
mod shared;
|
|
|
|
use anyhow::Result;
|
|
use args::*;
|
|
use clap::Parser;
|
|
use tracing::debug;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "fabro", version, long_version = LONG_VERSION)]
|
|
struct Cli {
|
|
#[command(flatten)]
|
|
globals: GlobalArgs,
|
|
|
|
#[command(subcommand)]
|
|
command: Box<Commands>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
fabro_telemetry::panic::install_panic_hook();
|
|
fabro_telemetry::init_cli();
|
|
|
|
let start = std::time::Instant::now();
|
|
let raw_args: Vec<String> = std::env::args().collect();
|
|
|
|
let (command_name, result) = main_inner().await;
|
|
let duration_ms = start.elapsed().as_millis() as u64;
|
|
|
|
let is_error = result.is_err();
|
|
let command = fabro_telemetry::sanitize::sanitize_command(&raw_args, &command_name);
|
|
let repository = fabro_telemetry::git::repository_identifier();
|
|
let ci = std::env::var("CI").is_ok();
|
|
if is_error {
|
|
fabro_telemetry::track!("CLI Errored", {
|
|
"subcommand": command_name,
|
|
"command": command,
|
|
"durationMs": duration_ms,
|
|
"repository": repository,
|
|
"ci": ci,
|
|
"success": false,
|
|
"exitCode": 1,
|
|
}, error);
|
|
} else {
|
|
fabro_telemetry::track!("CLI Executed", {
|
|
"subcommand": command_name,
|
|
"command": command,
|
|
"durationMs": duration_ms,
|
|
"repository": repository,
|
|
"ci": ci,
|
|
"success": true,
|
|
"exitCode": 0,
|
|
});
|
|
}
|
|
fabro_telemetry::shutdown();
|
|
|
|
if let Err(err) = result {
|
|
let style = console::Style::new().red().bold();
|
|
for (i, cause) in err.chain().enumerate() {
|
|
let text = cause.to_string();
|
|
if i == 0 {
|
|
for (j, line) in text.lines().enumerate() {
|
|
if j == 0 {
|
|
eprintln!("{} {line}", style.apply_to("error:"));
|
|
} else {
|
|
eprintln!(" {line}");
|
|
}
|
|
}
|
|
} else {
|
|
for line in text.lines() {
|
|
eprintln!(" > {line}");
|
|
}
|
|
}
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
async fn main_inner() -> (String, Result<()>) {
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
let cli = Cli::parse();
|
|
if let Some(home) = dirs::home_dir() {
|
|
let env_path = home.join(".fabro").join(".env");
|
|
if dotenvy::from_path(&env_path).is_ok() {
|
|
debug!(path = %env_path.display(), "Loaded environment file");
|
|
}
|
|
}
|
|
|
|
let Cli { globals, command } = cli;
|
|
let command_name = command.name().to_string();
|
|
|
|
let (config_log_level, upgrade_check_enabled) = {
|
|
#[cfg(feature = "server")]
|
|
{
|
|
if let Commands::Serve(args) = command.as_ref() {
|
|
match fabro_config::server::load_server_config(args.config.as_deref()) {
|
|
Ok(server_config) => (
|
|
server_config.log.as_ref().and_then(|l| l.level.clone()),
|
|
false,
|
|
),
|
|
Err(err) => return (command_name, Err(err)),
|
|
}
|
|
} else {
|
|
match fabro_config::cli::load_cli_config(None) {
|
|
Ok(cli_config) => (
|
|
cli_config.log.as_ref().and_then(|l| l.level.clone()),
|
|
cli_config.upgrade_check_enabled(),
|
|
),
|
|
Err(err) => return (command_name, Err(err)),
|
|
}
|
|
}
|
|
}
|
|
#[cfg(not(feature = "server"))]
|
|
{
|
|
match fabro_config::cli::load_cli_config(None) {
|
|
Ok(cli_config) => (
|
|
cli_config.log.as_ref().and_then(|l| l.level.clone()),
|
|
cli_config.upgrade_check_enabled(),
|
|
),
|
|
Err(err) => return (command_name, Err(err)),
|
|
}
|
|
}
|
|
};
|
|
|
|
let log_prefix = if command_name == "serve" {
|
|
"serve"
|
|
} else {
|
|
"cli"
|
|
};
|
|
if let Err(err) = logging::init_tracing(globals.debug, config_log_level.as_deref(), log_prefix)
|
|
{
|
|
eprintln!("Warning: failed to initialize logging: {err:#}");
|
|
}
|
|
|
|
debug!(command = %command_name, "CLI command started");
|
|
|
|
let upgrade_handle = if matches!(
|
|
command.as_ref(),
|
|
Commands::Run(_)
|
|
| Commands::Create(_)
|
|
| Commands::Exec(_)
|
|
| Commands::Repo(_)
|
|
| Commands::Init
|
|
| Commands::Install { .. }
|
|
) {
|
|
commands::upgrade::spawn_upgrade_check(globals.no_upgrade_check, upgrade_check_enabled)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let result = async move {
|
|
match *command {
|
|
Commands::Llm(ns) => commands::llm::dispatch(ns, &globals).await?,
|
|
Commands::Exec(args) => commands::exec::execute(args, &globals).await?,
|
|
Commands::Run(args) => commands::run::execute(args, &globals).await?,
|
|
Commands::Create(args) => {
|
|
let styles: &'static fabro_util::terminal::Styles =
|
|
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
|
let cli_config = cli_config::load_cli_config(None)?;
|
|
let (run_id, _run_dir) =
|
|
commands::create::create_run(&args, cli_config, styles, true).await?;
|
|
println!("{run_id}");
|
|
}
|
|
Commands::Start { run } => {
|
|
let base = fabro_workflows::run_lookup::default_runs_base();
|
|
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
|
|
let child = commands::start::start_run(&run_info.path, false)?;
|
|
eprintln!("Started engine process (PID {})", child.id());
|
|
}
|
|
Commands::Attach { run } => {
|
|
let styles: &'static fabro_util::terminal::Styles =
|
|
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
|
let base = fabro_workflows::run_lookup::default_runs_base();
|
|
let run_info = fabro_workflows::run_lookup::resolve_run(&base, &run)?;
|
|
let exit_code =
|
|
commands::attach::attach_run(&run_info.path, false, styles, None).await?;
|
|
if exit_code != std::process::ExitCode::SUCCESS {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
Commands::RunEngine { run_dir, resume } => {
|
|
commands::run_engine::execute(run_dir, resume).await?;
|
|
}
|
|
Commands::Validate(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stderr();
|
|
commands::validate::run(&args, &styles)?;
|
|
}
|
|
Commands::Graph(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stderr();
|
|
commands::graph::run(&args, &styles)?;
|
|
}
|
|
Commands::Parse(args) => {
|
|
commands::parse::run(&args)?;
|
|
}
|
|
Commands::Asset(ns) => commands::asset::dispatch(ns)?,
|
|
Commands::Cp(args) => {
|
|
commands::cp::cp_command(args).await?;
|
|
}
|
|
Commands::Preview(args) => {
|
|
commands::preview::run(args).await?;
|
|
}
|
|
Commands::Ssh(args) => {
|
|
commands::ssh::run(args).await?;
|
|
}
|
|
Commands::Diff(args) => {
|
|
commands::diff::run(args).await?;
|
|
}
|
|
Commands::Logs(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stdout();
|
|
commands::logs::run(args, &styles)?;
|
|
}
|
|
Commands::Inspect(args) => {
|
|
commands::inspect::run(&args)?;
|
|
}
|
|
Commands::Model { command } => commands::model::execute(command, &globals).await?,
|
|
#[cfg(feature = "server")]
|
|
Commands::Serve(args) => {
|
|
let styles: &'static fabro_util::terminal::Styles =
|
|
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
|
fabro_api::serve::serve_command(args, styles).await?;
|
|
}
|
|
Commands::Doctor { verbose, dry_run } => {
|
|
let cli_config = cli_config::load_cli_config(None)?;
|
|
let verbose = verbose || cli_config.verbose_enabled();
|
|
let exit_code = commands::doctor::run_doctor(verbose, !dry_run).await;
|
|
std::process::exit(exit_code);
|
|
}
|
|
Commands::Discord => {
|
|
open::that("https://fabro.sh/discord")?;
|
|
}
|
|
Commands::Docs => {
|
|
open::that("https://docs.fabro.sh/")?;
|
|
}
|
|
Commands::Repo(ns) => commands::repo::dispatch(ns).await?,
|
|
Commands::Init => {
|
|
eprintln!(
|
|
"{} `fabro init` is deprecated, use `fabro repo init` instead",
|
|
console::Style::new().yellow().apply_to("warning:")
|
|
);
|
|
commands::repo::init::run_init().await?;
|
|
}
|
|
Commands::Install { web_url } => {
|
|
commands::install::run_install(&web_url).await?;
|
|
}
|
|
Commands::Ps(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stdout();
|
|
commands::runs::list_command(&args, &styles)?;
|
|
}
|
|
Commands::Rm(args) => {
|
|
commands::runs::remove_command(&args).await?;
|
|
}
|
|
Commands::Pr(ns) => commands::pr::dispatch(ns).await?,
|
|
Commands::Secret(ns) => commands::secret::dispatch(ns)?,
|
|
Commands::Resume(args) => {
|
|
let styles: &'static fabro_util::terminal::Styles =
|
|
Box::leak(Box::new(fabro_util::terminal::Styles::detect_stderr()));
|
|
#[cfg(feature = "sleep_inhibitor")]
|
|
let _sleep_guard = {
|
|
let cli_config = cli_config::load_cli_config(None)?;
|
|
fabro_beastie::guard(cli_config.prevent_idle_sleep_enabled())
|
|
};
|
|
commands::resume::resume_command(args, styles).await?;
|
|
}
|
|
Commands::Rewind(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stderr();
|
|
commands::rewind::run(&args, &styles)?;
|
|
}
|
|
Commands::Fork(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stderr();
|
|
commands::fork::run(&args, &styles)?;
|
|
}
|
|
Commands::Wait(args) => {
|
|
let styles = fabro_util::terminal::Styles::detect_stderr();
|
|
commands::wait::run(args, &styles)?;
|
|
}
|
|
Commands::Workflow(ns) => commands::workflow::dispatch(ns)?,
|
|
Commands::Skill(ns) => commands::skill::dispatch(ns)?,
|
|
Commands::Upgrade(args) => {
|
|
commands::upgrade::run_upgrade(args).await?;
|
|
}
|
|
Commands::Provider(ns) => commands::provider::dispatch(ns).await?,
|
|
Commands::System(ns) => commands::system::dispatch(ns)?,
|
|
Commands::SendAnalytics { path } => {
|
|
let result = fabro_telemetry::sender::upload(&path).await;
|
|
let _ = std::fs::remove_file(&path);
|
|
result?;
|
|
}
|
|
Commands::SendPanic { path } => {
|
|
let result = fabro_telemetry::panic::capture(&path).await;
|
|
let _ = std::fs::remove_file(&path);
|
|
result?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
.await;
|
|
|
|
// Print upgrade notice after command completes (non-blocking during execution)
|
|
if let Some(handle) = upgrade_handle {
|
|
let _ = handle.await;
|
|
}
|
|
|
|
(command_name, result)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use clap::Parser;
|
|
|
|
#[test]
|
|
fn parse_provider_login_openai() {
|
|
let cli = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "openai"])
|
|
.expect("should parse");
|
|
match *cli.command {
|
|
Commands::Provider(ProviderNamespace {
|
|
command: ProviderCommand::Login(args),
|
|
}) => {
|
|
assert_eq!(args.provider, fabro_model::Provider::OpenAi);
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_provider_login_anthropic() {
|
|
let cli = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "anthropic"])
|
|
.expect("should parse");
|
|
match *cli.command {
|
|
Commands::Provider(ProviderNamespace {
|
|
command: ProviderCommand::Login(args),
|
|
}) => {
|
|
assert_eq!(args.provider, fabro_model::Provider::Anthropic);
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_provider_login_missing_provider_flag() {
|
|
let result = Cli::try_parse_from(["fabro", "provider", "login"]);
|
|
assert!(result.is_err(), "should fail without --provider");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_provider_login_bogus_provider() {
|
|
let result = Cli::try_parse_from(["fabro", "provider", "login", "--provider", "bogus"]);
|
|
assert!(result.is_err(), "should fail with unknown provider");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_create_command() {
|
|
let cli = Cli::try_parse_from(["fabro", "create", "my-workflow.toml", "--goal", "test"])
|
|
.expect("should parse");
|
|
match *cli.command {
|
|
Commands::Create(args) => {
|
|
assert_eq!(
|
|
args.workflow.as_deref(),
|
|
Some(std::path::Path::new("my-workflow.toml"))
|
|
);
|
|
assert_eq!(args.goal.as_deref(), Some("test"));
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_start_command() {
|
|
let cli = Cli::try_parse_from(["fabro", "start", "ABC123"]).expect("should parse");
|
|
match *cli.command {
|
|
Commands::Start { run } => {
|
|
assert_eq!(run, "ABC123");
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_attach_command() {
|
|
let cli = Cli::try_parse_from(["fabro", "attach", "ABC123"]).expect("should parse");
|
|
match *cli.command {
|
|
Commands::Attach { run } => {
|
|
assert_eq!(run, "ABC123");
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_run_engine_command() {
|
|
let cli = Cli::try_parse_from(["fabro", "_run_engine", "--run-dir", "/tmp/runs/test"])
|
|
.expect("should parse");
|
|
match *cli.command {
|
|
Commands::RunEngine { run_dir, resume } => {
|
|
assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test"));
|
|
assert!(!resume);
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_run_engine_with_resume() {
|
|
let cli = Cli::try_parse_from([
|
|
"fabro",
|
|
"_run_engine",
|
|
"--run-dir",
|
|
"/tmp/runs/test",
|
|
"--resume",
|
|
])
|
|
.expect("should parse");
|
|
match *cli.command {
|
|
Commands::RunEngine { run_dir, resume } => {
|
|
assert_eq!(run_dir, std::path::PathBuf::from("/tmp/runs/test"));
|
|
assert!(resume);
|
|
}
|
|
_ => panic!("unexpected command variant"),
|
|
}
|
|
}
|
|
}
|