mod cli;
mod commands;
mod formatters;
mod gh_identity;
mod types;
use clap::Parser;
use cli::{Cli, Command};
use commands::{
daemon::{restart, run_daemon, start, stop_daemon},
install::install,
launch::{connect, launch},
misc::{attach_cmd, coordinator, doctor, hook, optimizer, overseer, status},
project::project,
repair::repair_deploy,
services::services,
session::session,
telegram::telegram,
};
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_behavior_a.rs"]
mod tests_behavior_a;
#[cfg(test)]
#[path = "tests_behavior_b.rs"]
mod tests_behavior_b;
static HELP: std::sync::LazyLock<trusty_common::help::HelpConfig> =
std::sync::LazyLock::new(|| {
trusty_common::help::load_help(include_str!("../../../help.yaml"))
.expect("trusty-mpm help.yaml is bundled and valid")
});
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let argv: Vec<String> = std::env::args().collect();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
if matches!(
e.kind(),
clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument
) {
trusty_common::help::print_suggestion_hint(&argv, &HELP);
}
std::process::exit(e.exit_code());
}
};
#[cfg(feature = "daemon")]
let mut _daemon_log_guard: Option<tracing_appender::non_blocking::WorkerGuard> = None;
#[cfg(feature = "daemon")]
let mut _error_store: Option<trusty_common::error_capture::ErrorStore> = None;
if matches!(
cli.command,
Command::Daemon { .. } | Command::Supervisor { .. }
) {
#[cfg(feature = "daemon")]
{
let log_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?
.join(".trusty-mpm")
.join("logs");
std::fs::create_dir_all(&log_dir)?;
let file_appender = tracing_appender::rolling::daily(&log_dir, "trusty-mpm.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
_daemon_log_guard = Some(guard);
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into());
let file_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into());
let (capture_layer, store) = trusty_common::error_capture::bug_capture_layer(
"trusty-mpm",
trusty_common::error_capture::DEFAULT_CAPTURE_CAPACITY,
env!("CARGO_PKG_VERSION"),
);
_error_store = Some(store);
use tracing_subscriber::Layer as _;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_filter(env_filter),
)
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_filter(file_filter),
)
.with(capture_layer)
.init();
}
#[cfg(not(feature = "daemon"))]
{
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.with_writer(std::io::stderr)
.init();
}
}
let client = reqwest::Client::new();
let url = trusty_mpm::core::resolve_daemon_url(Some(&cli.url));
let result = match cli.command {
Command::Status => status(&client, &url).await,
Command::Start => start(&client, &url).await,
Command::Serve { stdio } => {
if stdio {
commands::serve_stdio::run_stdio_bridge().await
} else {
start(&client, &url).await
}
}
Command::Stop => stop_daemon().await,
Command::Restart => restart(&client, &url).await,
Command::Project { action } => project(&client, &url, action).await,
Command::Session { action } => session(&client, &url, action).await,
Command::Events => commands::misc::events(&client, &url).await,
Command::Doctor => doctor(&url).await,
Command::Tui {
url: tui_url,
interval_ms,
} => {
let resolved = trusty_mpm::core::resolve_daemon_url(Some(&tui_url));
trusty_mpm::tui::run(resolved, interval_ms).await
}
Command::Gui => launch_gui(),
Command::Telegram { cmd } => telegram(&url, cmd).await,
Command::Install { force } => install(force),
Command::Hook => hook(&client, &url).await,
Command::Daemon {
addr,
tailscale,
mcp,
} => run_daemon(addr, tailscale, mcp).await,
Command::Supervisor {
addr,
interval,
auto_resume,
no_classify,
} => commands::supervisor::run_supervisor(addr, interval, auto_resume, no_classify).await,
Command::Launch { dir } => launch(&client, &url, dir).await,
Command::Connect { dir } => connect(&client, &url, dir).await,
Command::Attach { target, json } => attach_cmd(&client, &url, &target, json).await,
Command::Optimizer { action } => optimizer(&client, &url, action).await,
Command::Overseer { action } => overseer(&client, &url, action).await,
Command::Coordinator { message, action } => {
match action {
Some(action) => commands::sm_serve::run_sm_serve(action).await,
None => match message {
Some(message) => coordinator(&url, message).await,
None => Err(anyhow::anyhow!(
"provide a message (`tm sm <message>`) or a subcommand \
(`tm sm serve --stdio`)"
)),
},
}
}
Command::Services { action } => services(action),
Command::Repair { action } => {
use cli::RepairAction;
match action {
RepairAction::Deploy { force } => repair_deploy(force),
}
}
Command::Catalog { action } => commands::managed::catalog(action).await,
Command::Ticket {
issue,
system,
notes,
runtime,
} => commands::ticket::ticket(&client, &url, issue, system, notes, runtime).await,
Command::Issue { cmd, system } => commands::issue::issue(cmd, system),
Command::Watch { cmd } => dispatch_watch(&client, &url, cmd).await,
};
if let Err(err) = &result
&& matches!(
err.downcast_ref::<commands::prune::PruneError>(),
Some(commands::prune::PruneError::SmUnavailable)
)
{
std::process::exit(commands::prune::EXIT_SM_UNAVAILABLE);
}
result
}
async fn dispatch_watch(
client: &reqwest::Client,
url: &str,
cmd: cli::WatchCmd,
) -> anyhow::Result<()> {
use cli::{WatchArgs, WatchCmd};
use commands::watch::args::RawWatchArgs;
fn raw(args: &WatchArgs) -> RawWatchArgs {
RawWatchArgs {
project: args.project.clone(),
label: args.label.clone(),
interval_secs: args.interval_secs,
state: args.state,
}
}
match cmd {
WatchCmd::Poll { args } => {
commands::watch::poll(
client,
url,
raw(&args),
args.execute,
args.dry_run,
args.runtime,
)
.await
}
WatchCmd::Listen { args } => {
commands::watch::listen(
client,
url,
raw(&args),
args.execute,
args.dry_run,
args.runtime,
)
.await
}
}
}
fn launch_gui() -> anyhow::Result<()> {
let program = resolve_gui_binary();
gui_status_to_result(std::process::Command::new(&program).status())
}
fn gui_status_to_result(status: std::io::Result<std::process::ExitStatus>) -> anyhow::Result<()> {
match status {
Ok(status) if status.success() => Ok(()),
Ok(status) => anyhow::bail!("trusty-mpm-gui exited with status: {status}"),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => anyhow::bail!(
"trusty-mpm-gui is not installed.\n\
Install it with: cargo install trusty-mpm-gui\n\
(the desktop GUI ships as a separate Tauri crate; `tm gui` launches it)"
),
Err(err) => Err(anyhow::Error::new(err).context("failed to launch trusty-mpm-gui")),
}
}
fn resolve_gui_binary() -> std::path::PathBuf {
const GUI_BIN: &str = "trusty-mpm-gui";
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
let sibling = dir.join(format!("{GUI_BIN}{}", std::env::consts::EXE_SUFFIX));
if sibling.is_file() {
return sibling;
}
}
std::path::PathBuf::from(GUI_BIN)
}