fabro/lib/crates/fabro-cli/src/main.rs
Bryan Helmkamp 3a4ff8a282
Add hidden --skill flag to fabro repo init
Allows skill installation during project setup via `fabro repo init --skill`,
which installs the fabro-create-workflow skill to .claude/skills/. The flag is
hidden from help output since `fabro skill install` is being deprecated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:54:30 -04:00

894 lines
32 KiB
Rust

mod cli_config;
mod commands;
mod doctor;
mod init;
mod install;
mod logging;
mod skill;
mod upgrade;
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing::debug;
const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("FABRO_GIT_SHA"),
" ",
env!("FABRO_BUILD_DATE"),
")"
);
#[derive(Parser)]
#[command(name = "fabro", version, long_version = LONG_VERSION)]
struct Cli {
/// Enable DEBUG-level logging (default is INFO)
#[arg(long, global = true)]
debug: bool,
/// Disable automatic upgrade check
#[arg(long, global = true)]
no_upgrade_check: bool,
/// Execution mode: standalone (in-process) or server (delegate to API)
#[cfg(feature = "server")]
#[arg(long, global = true, value_parser = parse_execution_mode)]
mode: Option<cli_config::ExecutionMode>,
/// Server URL (overrides server.base_url from cli.toml)
#[cfg(feature = "server")]
#[arg(long, global = true)]
server_url: Option<String>,
#[command(subcommand)]
command: Command,
}
#[cfg(feature = "server")]
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'"
)),
}
}
#[derive(Subcommand)]
enum Command {
/// LLM prompt operations
#[command(hide = true)]
Llm {
#[command(subcommand)]
command: LlmCommand,
},
/// Run an agentic coding session
#[command(hide = true)]
Exec(fabro_agent::cli::AgentArgs),
/// Launch a workflow run
Run(commands::run::RunArgs),
/// Validate a workflow
Validate(commands::validate::ValidateArgs),
/// Render a workflow graph as SVG or PNG
Graph(commands::graph::GraphArgs),
/// Parse a DOT file and print its AST
#[command(hide = true)]
Parse(commands::parse::ParseArgs),
/// Inspect and copy run assets (screenshots, reports, traces)
Asset {
#[command(subcommand)]
command: AssetCommand,
},
/// Copy files to/from a run's sandbox
Cp(commands::cp::CpArgs),
/// Get a preview URL for a port on a run's sandbox
Preview(commands::preview::PreviewArgs),
/// SSH into a run's Daytona sandbox
Ssh(commands::ssh::SshArgs),
/// Show the diff of changes from a workflow run
#[command(hide = true)]
Diff(commands::diff::DiffArgs),
/// View the event log of a workflow run
Logs(commands::logs::LogsArgs),
/// Show detailed information about a workflow run
Inspect(commands::inspect::InspectArgs),
/// List and test LLM models
Model {
#[command(subcommand)]
command: Option<fabro_llm::cli::ModelsCommand>,
},
/// Start the HTTP API server
#[cfg(feature = "server")]
Serve(fabro_api::serve::ServeArgs),
/// Check environment and integration health
Doctor {
/// Show detailed information for each check
#[arg(short, long)]
verbose: bool,
/// Skip live service probes (LLM, sandbox, API, web, Brave Search)
#[arg(long)]
dry_run: bool,
},
/// Initialize a new project (deprecated: use `repo init`)
#[command(hide = true)]
Init,
/// Set up the Fabro environment (LLMs, certs, GitHub)
Install,
/// List workflow runs
#[command(hide = true)]
Ps(commands::runs::RunsListArgs),
/// Remove one or more workflow runs
Rm(commands::runs::RunsRemoveArgs),
/// Pull request operations
Pr {
#[command(subcommand)]
command: PrCommand,
},
/// Skill management
#[command(hide = true)]
Skill {
#[command(subcommand)]
command: SkillCommand,
},
/// Rewind a workflow run to an earlier checkpoint
Rewind(commands::rewind::RewindArgs),
/// Fork a workflow run from an earlier checkpoint into a new run
Fork(commands::fork::ForkArgs),
/// Workflow operations
Workflow {
#[command(subcommand)]
command: WorkflowCommand,
},
/// Open the Discord community in the browser
Discord,
/// Open the docs website in the browser
Docs,
/// Upgrade fabro to the latest version
Upgrade(upgrade::UpgradeArgs),
/// Repository commands
Repo {
#[command(subcommand)]
command: RepoCommand,
},
/// System maintenance commands
System {
#[command(subcommand)]
command: SystemCommand,
},
/// Send a queued analytics event (internal)
#[command(name = "__send_analytics", hide = true)]
SendAnalytics {
/// Path to the JSON event file
path: PathBuf,
},
/// Send a queued panic event to Sentry (internal)
#[command(name = "__send_panic", hide = true)]
SendPanic {
/// Path to the JSON event file
path: PathBuf,
},
}
#[derive(Subcommand)]
enum PrCommand {
/// Create a pull request from a completed run
Create(commands::pr::PrCreateArgs),
/// List pull requests from workflow runs
List(commands::pr::PrListArgs),
/// View pull request details
View(commands::pr::PrViewArgs),
/// Merge a pull request
Merge(commands::pr::PrMergeArgs),
/// Close a pull request
Close(commands::pr::PrCloseArgs),
}
#[derive(Subcommand)]
enum SystemCommand {
/// Delete old workflow runs
Prune(commands::runs::RunsPruneArgs),
/// Show disk usage
Df(commands::runs::DfArgs),
}
#[derive(Subcommand)]
enum RepoCommand {
/// Initialize a new project
Init {
/// Also install the fabro-create-workflow skill
#[arg(long, hide = true)]
skill: bool,
},
/// Remove fabro.toml and fabro/ directory
Deinit,
}
#[derive(Subcommand)]
enum SkillCommand {
/// Install a built-in skill
Install(skill::SkillInstallArgs),
}
#[derive(Subcommand)]
enum WorkflowCommand {
/// List available workflows
List(commands::workflow::WorkflowListArgs),
/// Create a new workflow
Create(commands::workflow::WorkflowCreateArgs),
}
#[derive(Subcommand)]
enum AssetCommand {
/// List assets for a workflow run
List(commands::asset::AssetListArgs),
/// Copy assets from a workflow run
Cp(commands::asset::AssetCpArgs),
}
#[derive(Subcommand)]
enum LlmCommand {
/// Execute a prompt
Prompt(fabro_llm::cli::PromptArgs),
/// Interactive multi-turn chat
Chat(fabro_llm::cli::ChatArgs),
}
pub(crate) fn build_github_app_credentials(
app_id: Option<&str>,
) -> Option<fabro_github::GitHubAppCredentials> {
let app_id = app_id?;
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?;
let private_key_pem = if raw.starts_with("-----") {
raw
} else {
let pem_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &raw).ok()?;
String::from_utf8(pem_bytes).ok()?
};
Some(fabro_github::GitHubAppCredentials {
app_id: app_id.to_string(),
private_key_pem,
})
}
/// Fork the workflow as a background process, print the run ID, and exit.
fn detach_run(args: commands::run::RunArgs) -> Result<()> {
let run_id = ulid::Ulid::new().to_string();
let run_dir = args.run_dir.clone().unwrap_or_else(|| {
let base = dirs::home_dir()
.expect("could not determine home directory")
.join(".fabro")
.join("runs");
base.join(format!(
"{}-{}",
chrono::Local::now().format("%Y%m%d"),
run_id
))
});
std::fs::create_dir_all(&run_dir)?;
std::fs::write(run_dir.join("id.txt"), &run_id)?;
fabro_workflows::run_status::write_run_status(
&run_dir,
fabro_workflows::run_status::RunStatus::Submitted,
None,
);
std::fs::File::create(run_dir.join("progress.jsonl"))?;
let log_file = std::fs::File::create(run_dir.join("detach.log"))?;
// Rebuild argv: current exe + original args, stripping --detach/-d, injecting --run-id and --run-dir
let exe = std::env::current_exe()?;
let mut child_args: Vec<String> = Vec::new();
child_args.push("run".to_string());
let raw_args: Vec<String> = std::env::args().collect();
// Skip argv[0] (binary) and argv[1] ("run"), then filter out --detach / -d
let mut iter = raw_args.iter().skip(2).peekable();
while let Some(arg) = iter.next() {
if arg == "--detach" || arg == "-d" {
continue;
}
// Skip --run-dir and its value (we'll override it)
if arg == "--run-dir" {
iter.next(); // consume the value
continue;
}
if arg.starts_with("--run-dir=") {
continue;
}
// Skip --run-id and its value (we'll override it)
if arg == "--run-id" {
iter.next();
continue;
}
if arg.starts_with("--run-id=") {
continue;
}
child_args.push(arg.clone());
}
child_args.push("--run-id".to_string());
child_args.push(run_id.clone());
child_args.push("--run-dir".to_string());
child_args.push(run_dir.to_string_lossy().to_string());
let mut cmd = std::process::Command::new(&exe);
cmd.args(&child_args)
.stdout(log_file.try_clone()?)
.stderr(log_file)
.stdin(std::process::Stdio::null());
// Detach from the controlling terminal on unix
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
libc::setsid();
Ok(())
});
}
}
cmd.spawn()?;
println!("{run_id}");
Ok(())
}
#[tokio::main]
async fn main() {
fabro_util::telemetry::panic::install_panic_hook();
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;
send_telemetry_event(&raw_args, &command_name, duration_ms, &result);
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);
}
}
fn send_telemetry_event(
raw_args: &[String],
command_name: &str,
duration_ms: u64,
result: &Result<()>,
) {
let is_error = result.is_err();
let telemetry = match fabro_util::telemetry::Telemetry::for_cli() {
Ok(t) => t,
Err(err) => {
debug!(%err, "Telemetry initialization failed");
return;
}
};
if !telemetry.should_track(is_error) {
return;
}
let event_name = if is_error {
"Command Error"
} else {
"Command Run"
};
let properties = serde_json::json!({
"subcommand": command_name,
"command": fabro_util::telemetry::sanitize::sanitize_command(raw_args, command_name),
"durationMs": duration_ms,
"repository": fabro_util::telemetry::git::repository_identifier(),
"ci": std::env::var("CI").is_ok(),
"success": !is_error,
"exitCode": if is_error { 1 } else { 0 },
});
let track = telemetry.build_track(event_name, properties);
fabro_util::telemetry::sender::send(track);
debug!(
event = event_name,
subcommand = command_name,
"Telemetry event queued"
);
}
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 command_name = match &cli.command {
Command::Llm { command } => match command {
LlmCommand::Prompt(_) => "llm prompt",
LlmCommand::Chat(_) => "llm chat",
},
Command::Asset { command } => match command {
AssetCommand::List(_) => "asset list",
AssetCommand::Cp(_) => "asset cp",
},
Command::Exec(_) => "exec",
Command::Run(_) => "run",
Command::Validate(_) => "validate",
Command::Graph(_) => "graph",
Command::Parse(_) => "parse",
Command::Cp(_) => "cp",
Command::Preview(_) => "preview",
Command::Ssh(_) => "ssh",
Command::Diff(_) => "diff",
Command::Logs(_) => "logs",
Command::Inspect(_) => "inspect",
Command::Model { command } => match command {
Some(fabro_llm::cli::ModelsCommand::List { .. }) => "model list",
Some(fabro_llm::cli::ModelsCommand::Test { .. }) => "model test",
None => "model",
},
#[cfg(feature = "server")]
Command::Serve(_) => "serve",
Command::Doctor { .. } => "doctor",
Command::Repo { command } => match command {
RepoCommand::Init { .. } => "repo init",
RepoCommand::Deinit => "repo deinit",
},
Command::Init => "init",
Command::Install => "install",
Command::Ps(_) => "ps",
Command::Rm(_) => "rm",
Command::Pr { command } => match command {
PrCommand::Create(_) => "pr create",
PrCommand::List(_) => "pr list",
PrCommand::View(_) => "pr view",
PrCommand::Merge(_) => "pr merge",
PrCommand::Close(_) => "pr close",
},
Command::Rewind(_) => "rewind",
Command::Fork(_) => "fork",
Command::Workflow { command } => match command {
WorkflowCommand::List(_) => "workflow list",
WorkflowCommand::Create(_) => "workflow create",
},
Command::Skill { command } => match command {
SkillCommand::Install(_) => "skill install",
},
Command::Discord => "discord",
Command::Docs => "docs",
Command::Upgrade(_) => "upgrade",
Command::System { command } => match command {
SystemCommand::Prune(_) => "system prune",
SystemCommand::Df(_) => "system df",
},
Command::SendAnalytics { .. } => "__send_analytics",
Command::SendPanic { .. } => "__send_panic",
};
let command_name = command_name.to_string();
let (config_log_level, upgrade_check_enabled) = {
#[cfg(feature = "server")]
{
if let Command::Serve(ref args) = cli.command {
match fabro_config::server::load_server_config(args.config.as_deref()) {
Ok(server_config) => (server_config.log.level, false),
Err(err) => return (command_name, Err(err)),
}
} else {
match fabro_config::cli::load_cli_config(None) {
Ok(cli_config) => (cli_config.log.level, cli_config.upgrade_check),
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.level, cli_config.upgrade_check),
Err(err) => return (command_name, Err(err)),
}
}
};
let log_prefix = if command_name == "serve" {
"serve"
} else {
"cli"
};
if let Err(err) = logging::init_tracing(cli.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!(
cli.command,
Command::Run(_)
| Command::Exec(_)
| Command::Repo { .. }
| Command::Init
| Command::Install
) {
upgrade::spawn_upgrade_check(cli.no_upgrade_check, upgrade_check_enabled)
} else {
None
};
let result = async {
match cli.command {
Command::Llm { command } => {
let cli_config = cli_config::load_cli_config(None)?;
let llm_defaults = cli_config.run_defaults.llm.as_ref();
match command {
LlmCommand::Prompt(mut args) => {
if args.model.is_none() {
args.model = llm_defaults.and_then(|l| l.model.clone());
}
#[cfg(feature = "server")]
{
let resolved = cli_config::resolve_mode(
cli.mode,
cli.server_url.as_deref(),
&cli_config,
);
match resolved.mode {
cli_config::ExecutionMode::Server => {
let client =
cli_config::build_server_client(resolved.tls.as_ref())?;
let server = fabro_llm::cli::ServerConnection {
client,
base_url: resolved.server_base_url,
};
fabro_llm::cli::run_prompt_via_server(args, &server).await?
}
cli_config::ExecutionMode::Standalone => {
fabro_llm::cli::run_prompt(args).await?
}
}
}
#[cfg(not(feature = "server"))]
{
fabro_llm::cli::run_prompt(args).await?
}
}
LlmCommand::Chat(mut args) => {
if args.model.is_none() {
args.model = llm_defaults.and_then(|l| l.model.clone());
}
#[cfg(feature = "server")]
{
let resolved = cli_config::resolve_mode(
cli.mode,
cli.server_url.as_deref(),
&cli_config,
);
match resolved.mode {
cli_config::ExecutionMode::Server => {
let client =
cli_config::build_server_client(resolved.tls.as_ref())?;
let server = fabro_llm::cli::ServerConnection {
client,
base_url: resolved.server_base_url,
};
fabro_llm::cli::run_chat_via_server(args, &server).await?
}
cli_config::ExecutionMode::Standalone => {
fabro_llm::cli::run_chat(args).await?
}
}
}
#[cfg(not(feature = "server"))]
{
fabro_llm::cli::run_chat(args).await?
}
}
}
}
Command::Exec(mut args) => {
let cli_config = cli_config::load_cli_config(None)?;
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = fabro_beastie::guard(cli_config.prevent_idle_sleep);
let exec_defaults = cli_config.exec.as_ref();
args.apply_cli_defaults(
exec_defaults.and_then(|a| a.provider.as_deref()),
exec_defaults.and_then(|a| a.model.as_deref()),
exec_defaults.and_then(|a| a.permissions),
exec_defaults.and_then(|a| a.output_format),
);
#[cfg(feature = "server")]
let resolved =
cli_config::resolve_mode(cli.mode, cli.server_url.as_deref(), &cli_config);
let mcp_servers: Vec<fabro_mcp::config::McpServerConfig> = cli_config
.run_defaults
.mcp_servers
.into_iter()
.map(|(name, entry)| entry.into_config(name))
.collect();
#[cfg(feature = "server")]
{
match resolved.mode {
cli_config::ExecutionMode::Server => {
tracing::info!(mode = "server", "Agent session starting");
let http_client =
cli_config::build_server_client(resolved.tls.as_ref())?;
let provider_name = args
.provider
.clone()
.unwrap_or_else(|| "anthropic".to_string());
let adapter =
std::sync::Arc::new(fabro_llm::providers::FabroServerAdapter::new(
http_client,
&resolved.server_base_url,
&provider_name,
));
let mut client = fabro_llm::client::Client::new(
std::collections::HashMap::new(),
None,
vec![],
);
client.register_provider(adapter).await.map_err(|e| {
anyhow::anyhow!("Failed to register fabro server adapter: {e}")
})?;
fabro_agent::cli::run_with_args_and_client(
args,
Some(client),
mcp_servers,
)
.await?
}
cli_config::ExecutionMode::Standalone => {
tracing::info!(mode = "standalone", "Agent session starting");
fabro_agent::cli::run_with_args(args, mcp_servers).await?
}
}
}
#[cfg(not(feature = "server"))]
{
tracing::info!(mode = "standalone", "Agent session starting");
fabro_agent::cli::run_with_args(args, mcp_servers).await?
}
}
Command::Run(mut args) => {
if args.detach {
return detach_run(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)?;
args.verbose = args.verbose || cli_config.verbose;
let github_app = build_github_app_credentials(cli_config.app_id());
let git_author = fabro_workflows::git::GitAuthor::from_options(
cli_config.git_author().and_then(|a| a.name.clone()),
cli_config.git_author().and_then(|a| a.email.clone()),
);
#[cfg(feature = "sleep_inhibitor")]
let _sleep_guard = fabro_beastie::guard(cli_config.prevent_idle_sleep);
commands::run::run_command(
args,
cli_config.run_defaults,
styles,
github_app,
git_author,
)
.await?;
}
Command::Validate(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::validate::run(&args, &styles)?;
}
Command::Graph(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::graph::run(&args, &styles)?;
}
Command::Parse(args) => {
commands::parse::run(&args)?;
}
Command::Asset { command } => match command {
AssetCommand::List(args) => {
commands::asset::list_command(&args)?;
}
AssetCommand::Cp(args) => {
commands::asset::cp_command(&args)?;
}
},
Command::Cp(args) => {
commands::cp::cp_command(args).await?;
}
Command::Preview(args) => {
commands::preview::run(args).await?;
}
Command::Ssh(args) => {
commands::ssh::run(args).await?;
}
Command::Diff(args) => {
commands::diff::run(args).await?;
}
Command::Logs(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
commands::logs::run(args, &styles)?;
}
Command::Inspect(args) => {
commands::inspect::run(&args)?;
}
Command::Model { command } => {
let server = {
#[cfg(feature = "server")]
{
let cli_config = cli_config::load_cli_config(None)?;
let resolved = cli_config::resolve_mode(
cli.mode,
cli.server_url.as_deref(),
&cli_config,
);
match resolved.mode {
cli_config::ExecutionMode::Server => {
let client =
cli_config::build_server_client(resolved.tls.as_ref())?;
Some(fabro_llm::cli::ServerConnection {
client,
base_url: resolved.server_base_url,
})
}
cli_config::ExecutionMode::Standalone => None,
}
}
#[cfg(not(feature = "server"))]
{
None
}
};
fabro_llm::cli::run_models(command, server).await?
}
#[cfg(feature = "server")]
Command::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?;
}
Command::Doctor { verbose, dry_run } => {
let cli_config = cli_config::load_cli_config(None)?;
let verbose = verbose || cli_config.verbose;
let exit_code = doctor::run_doctor(verbose, !dry_run).await;
std::process::exit(exit_code);
}
Command::Discord => {
open::that("https://fabro.sh/discord")?;
}
Command::Docs => {
open::that("https://docs.fabro.sh/")?;
}
Command::Repo { command } => match command {
RepoCommand::Init { skill } => {
init::run_init().await?;
if skill {
let base = std::env::current_dir()?.join(".claude").join("skills");
skill::install_skill_to(&base)?;
}
}
RepoCommand::Deinit => {
init::run_deinit()?;
}
},
Command::Init => {
eprintln!(
"{} `fabro init` is deprecated, use `fabro repo init` instead",
console::Style::new().yellow().apply_to("warning:")
);
init::run_init().await?;
}
Command::Install => {
install::run_install().await?;
}
Command::Ps(args) => {
let styles = fabro_util::terminal::Styles::detect_stdout();
commands::runs::list_command(&args, &styles)?;
}
Command::Rm(args) => {
commands::runs::remove_command(&args).await?;
}
Command::Pr { command } => {
let cli_config = cli_config::load_cli_config(None)?;
let github_app = build_github_app_credentials(cli_config.app_id());
match command {
PrCommand::Create(args) => {
commands::pr::create_command(args, github_app).await?;
}
PrCommand::List(args) => {
commands::pr::list_command(args, github_app).await?;
}
PrCommand::View(args) => {
commands::pr::view_command(args, github_app).await?;
}
PrCommand::Merge(args) => {
commands::pr::merge_command(args, github_app).await?;
}
PrCommand::Close(args) => {
commands::pr::close_command(args, github_app).await?;
}
}
}
Command::Rewind(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::rewind::run(&args, &styles)?;
}
Command::Fork(args) => {
let styles = fabro_util::terminal::Styles::detect_stderr();
commands::fork::run(&args, &styles)?;
}
Command::Workflow { command } => match command {
WorkflowCommand::List(args) => {
commands::workflow::list_command(&args)?;
}
WorkflowCommand::Create(args) => {
commands::workflow::create_command(&args)?;
}
},
Command::Skill { command } => match command {
SkillCommand::Install(args) => {
skill::run_skill_install(&args)?;
}
},
Command::Upgrade(args) => {
upgrade::run_upgrade(args).await?;
}
Command::System { command } => match command {
SystemCommand::Prune(args) => {
commands::runs::prune_command(&args)?;
}
SystemCommand::Df(args) => {
commands::runs::df_command(&args)?;
}
},
Command::SendAnalytics { path } => {
let result = fabro_util::telemetry::sender::send_to_segment(&path).await;
let _ = std::fs::remove_file(&path);
result?;
}
Command::SendPanic { path } => {
let result = fabro_util::telemetry::panic::send_panic_to_sentry(&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)
}