#![deny(rustdoc::broken_intra_doc_links)]
mod cli;
mod cli_manager;
mod commands;
mod formatters;
mod generate;
mod gh_identity;
mod tracing_setup;
mod types;
use std::io::IsTerminal as _;
use clap::Parser;
use cli::{Cli, Command};
use commands::{
agent::agent,
compress::run_compress,
daemon::{restart, run_daemon, start, stop_daemon},
generate::generate,
hooks::clean as hooks_clean,
install::install,
launch::{connect, launch},
manager::manager,
memory::memory,
misc::{attach_cmd, coordinator, doctor, health, hook, optimizer, overseer, status, validate},
project::project,
projects::projects,
services::services,
session::session,
slack::slack,
telegram::telegram,
};
#[cfg(test)]
#[path = "test_support.rs"]
mod test_support;
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_behavior_a.rs"]
mod tests_behavior_a;
#[cfg(test)]
#[path = "install_policy_tests.rs"]
mod install_policy_tests;
#[cfg(test)]
#[path = "tests_behavior_b_tests.rs"]
mod tests_behavior_b;
#[cfg(test)]
#[path = "tests_behavior_c_tests.rs"]
mod tests_behavior_c;
#[cfg(test)]
#[path = "tests_behavior_d_tests.rs"]
mod tests_behavior_d;
#[cfg(test)]
#[path = "tests_behavior_e_tests.rs"]
mod tests_behavior_e;
#[cfg(test)]
#[path = "tests_behavior_generate_tests.rs"]
mod tests_behavior_generate;
#[cfg(test)]
#[path = "tests_behavior_hooks_tests.rs"]
mod tests_behavior_hooks;
#[cfg(test)]
#[path = "tests_behavior_repair_tests.rs"]
mod tests_behavior_repair;
#[cfg(test)]
#[path = "tests_behavior_reset_agents_tests.rs"]
mod tests_behavior_reset_agents;
#[cfg(test)]
#[path = "tests_behavior_skill_tiers_tests.rs"]
mod tests_behavior_skill_tiers;
#[cfg(test)]
#[path = "tests_behavior_2890_skills_tests.rs"]
mod tests_behavior_2890_skills;
#[cfg(test)]
#[path = "tests_behavior_2903_skills_tests.rs"]
mod tests_behavior_2903_skills;
#[cfg(test)]
#[path = "tests_behavior_2911_documentation_style_tests.rs"]
mod tests_behavior_2911_documentation_style;
#[cfg(test)]
#[path = "tests_behavior_rust_build_performance_tests.rs"]
mod tests_behavior_rust_build_performance;
#[cfg(test)]
#[path = "tests_projects.rs"]
mod tests_projects;
#[cfg(test)]
#[path = "tests_projects_config_tests.rs"]
mod tests_projects_config_tests;
#[cfg(test)]
#[path = "tests_manager.rs"]
mod tests_manager;
#[cfg(test)]
#[path = "tests_project_trust_tests.rs"]
mod tests_project_trust;
#[cfg(test)]
#[path = "env_isolation_tests.rs"]
mod env_isolation_tests;
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();
if commands::projects::should_launch_bare_tui(
&argv,
std::io::stdin().is_terminal(),
std::io::stdout().is_terminal(),
) {
return commands::projects::launch_bare_tui().await;
}
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());
}
};
if let Some(Command::Config(cmd)) = cli.command {
return cmd.run().await;
}
if let Some(Command::InternalSpawnDisclaimed { argv: spawn_argv }) = cli.command {
if !commands::spawn_disclaimed::invoked_literally(&argv) {
eprintln!(
"error: '{}' must be invoked by its exact name, not an abbreviation",
trusty_mpm::core::spawn_disclaim::PANE_DISCLAIM_SUBCOMMAND
);
std::process::exit(2);
}
return commands::spawn_disclaimed::run(spawn_argv);
}
#[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;
tracing_setup::init_cli_diagnostics_if_wanted(&cli.command);
if matches!(
cli.command,
Some(Command::Daemon { .. }) | Some(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_setup::init_stderr_only("info");
}
{
let paths = trusty_mpm::core::paths::FrameworkPaths::default();
trusty_mpm::core::stale_skills::run_stale_mpm_skills_migration_once(
&paths.root,
&paths.claude_skills_dir(),
);
}
let client = trusty_mpm::client::http_client::default_client();
let url = trusty_mpm::core::resolve_daemon_url_via_gateway(&client, cli.url.as_deref()).await;
let result = match cli.command {
None => commands::guided::run_guided_default(&client, &url, cli.url.as_deref()).await,
Some(Command::Status) => status(&client, &url).await,
Some(Command::Start) => start(&client, &url).await,
Some(Command::Serve { stdio }) => {
if stdio {
commands::serve_stdio::run_stdio_bridge().await
} else {
start(&client, &url).await
}
}
Some(Command::Stop) => stop_daemon().await,
Some(Command::Restart) => restart(&client, &url).await,
Some(Command::Project { action }) => project(&client, &url, action).await,
Some(Command::Sessions { action }) => session(&client, &url, action).await,
Some(Command::Projects { action }) => projects(&client, &url, action).await,
Some(Command::Manager { action }) => manager(&client, &url, action).await,
Some(Command::Session { action }) => {
commands::session::emit_top_level_alias_notice();
session(&client, &url, action).await
}
Some(Command::Events) => commands::misc::events(&client, &url).await,
Some(Command::Doctor { flags }) => doctor(&url, &flags).await,
Some(Command::Validate { path, repair }) => validate(path, repair).await,
Some(Command::Hooks { action }) => {
use cli::HooksAction;
match action {
HooksAction::Clean { path, force } => hooks_clean(path, force),
}
}
Some(Command::Agent { action }) => agent(action).await,
Some(Command::Generate { action }) => generate(action).await,
Some(Command::Health) => health(&url).await,
Some(Command::Tui {
url: tui_url,
interval_ms,
}) => {
let resolved = trusty_mpm::core::resolve_daemon_url(tui_url.as_deref());
trusty_mpm::tui::run(resolved, interval_ms).await
}
Some(Command::Gui) => launch_gui(),
Some(Command::Telegram { cmd }) => telegram(&url, cmd).await,
Some(Command::Slack { cmd }) => slack(cmd).await,
Some(Command::Install {
force,
reset_agents,
reset_agents_workspaces,
reconcile_skills,
}) => {
install(
force,
reset_agents,
reset_agents_workspaces,
reconcile_skills,
)
.await
}
Some(Command::Reinstall(args)) => commands::reinstall::run(args).await,
Some(Command::Hook { pm_guard }) => {
if pm_guard {
commands::pm_guard::pm_guard(&url).await
} else {
hook(&client, &url).await
}
}
Some(Command::Memory { action }) => memory(action).await,
Some(Command::Compress { tool }) => run_compress(&tool).await,
Some(Command::Daemon {
addr,
tailscale,
mcp,
force,
}) => run_daemon(addr, tailscale, mcp, force).await,
Some(Command::Supervisor {
addr,
interval,
auto_resume,
no_classify,
}) => commands::supervisor::run_supervisor(addr, interval, auto_resume, no_classify).await,
Some(Command::Launch {
dir,
style,
worktree,
}) => launch(&client, &url, dir, style, worktree).await,
Some(Command::Connect { dir }) => connect(&client, &url, dir).await,
Some(Command::Attach { target, json }) => attach_cmd(&client, &url, &target, json).await,
Some(Command::Optimizer { action }) => optimizer(&client, &url, action).await,
Some(Command::Overseer { action }) => overseer(&client, &url, action).await,
Some(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`)"
)),
},
}
}
Some(Command::Services { action }) => services(action),
Some(Command::Repair { action }) => commands::repair::dispatch(action),
Some(Command::Auth { action }) => {
use cli::AuthAction;
match action {
AuthAction::SetToken { token, stdin } => commands::auth::set_token(token, stdin),
AuthAction::ClearToken => commands::auth::clear_token(),
AuthAction::Status => commands::auth::status(),
}
}
Some(Command::Catalog { action }) => commands::managed::catalog(action).await,
Some(Command::Ticket {
issue,
system,
notes,
runtime,
}) => commands::ticket::ticket(&client, &url, issue, system, notes, runtime).await,
Some(Command::Issue { cmd, system }) => commands::issue::issue(cmd, system),
Some(Command::Watch { cmd }) => dispatch_watch(&client, &url, cmd).await,
Some(Command::Meta { action }) => commands::meta::meta(action).await,
Some(Command::Register {
url,
alias,
force,
root,
}) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::register_cmd(&paths, &url, alias.as_deref(), force)
}
Some(Command::Ls {
terms,
projects,
json,
source_id,
current,
all,
attached,
root,
}) => {
if projects {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::ls_cmd(&paths, json)
} else {
let (sort, term) = commands::session_picker::parse_ls_terms(&terms);
let filter = term.map(commands::session_picker::SessionFilter::visible);
commands::session_ls_connector::run_ls_connector(
&client, &url, json, source_id, current, all, attached, sort, filter,
)
.await
}
}
Some(Command::F(args)) => {
commands::session_picker_filter::run_f_command(&client, &url, args).await
}
Some(Command::Load { alias, root }) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::load_cmd(&paths, &alias)
}
Some(Command::Run { target, task, root }) => {
commands::run_target::run(&client, &url, &target, task, root).await
}
Some(Command::Path { alias, root }) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::path_cmd(&paths, &alias)
}
Some(Command::ShellInit { shell }) => commands::shell_init::run_shell_init(shell),
Some(Command::Login { root }) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::login_cmd(&paths)
}
Some(Command::Rm { alias, root }) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::rm_cmd(&paths, &alias)
}
Some(Command::Update { alias, root }) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::update_cmd(&paths, alias.as_deref())
}
Some(Command::Sessctl { action }) => {
commands::sessctl::dispatch(&client, &url, action).await
}
Some(Command::Statusline) => commands::statusline::run_statusline(),
Some(Command::Banner { reconnecting }) => {
commands::banner::run_banner_preview(reconnecting)
}
Some(Command::Mcp { cmd }) => match cmd {
cli::McpCmd::Add {
name,
transport,
env,
header,
command_and_args,
root,
} => commands::mcp::add_cmd(
root.as_deref(),
&name,
transport,
&env,
&header,
&command_and_args,
),
cli::McpCmd::Remove { name, root } => commands::mcp::remove_cmd(root.as_deref(), &name),
cli::McpCmd::List { json, root } => commands::mcp::list_cmd(root.as_deref(), json),
cli::McpCmd::Get { name, json, root } => {
commands::mcp::get_cmd(root.as_deref(), &name, json)
}
cli::McpCmd::Test { name, json, root } => {
commands::mcp::test_cmd(root.as_deref(), name.as_deref(), json).await
}
},
Some(Command::Config(_)) => unreachable!("config dispatched before daemon-URL resolution"),
Some(Command::InternalSpawnDisclaimed { .. }) => {
unreachable!("internal-spawn-disclaimed dispatched before daemon-URL resolution")
}
};
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);
}
if let Err(err) = &result
&& err
.downcast_ref::<trusty_mpm::core::DaemonUrlError>()
.is_some()
{
std::process::exit(trusty_mpm::core::exit_codes::EXIT_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)
}