#![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;
mod watch_dispatch;
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},
managed_workspace::LaunchDir,
manager::manager,
memory::memory,
misc::{attach_cmd, coordinator, hook, optimizer, overseer, validate},
project::project,
projects::projects,
services::services,
slack::slack,
telegram::telegram,
};
use trusty_mpm::client::DaemonClient;
#[cfg(test)]
#[path = "test_support.rs"]
mod test_support;
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tests_behavior_a_tests.rs"]
mod tests_behavior_a;
#[cfg(test)]
#[path = "memory_import_cli_tests.rs"]
mod memory_import_cli_tests;
#[cfg(test)]
#[path = "divert_cli_tests.rs"]
mod divert_cli_tests;
#[cfg(test)]
#[path = "install_policy_tests.rs"]
mod install_policy_tests;
#[cfg(test)]
#[path = "tests_behavior_bare_run_tests.rs"]
mod tests_behavior_bare_run;
#[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_d_ls_connector_tests.rs"]
mod tests_behavior_d_ls_connector;
#[cfg(test)]
#[path = "tests_behavior_d_stop_delete_tests.rs"]
mod tests_behavior_d_stop_delete;
#[cfg(test)]
#[path = "tests_behavior_d_f_cli_tests.rs"]
mod tests_behavior_d_f_cli;
#[cfg(test)]
#[path = "tests_behavior_rebind_tests.rs"]
mod tests_behavior_rebind;
#[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_install_skills_tests.rs"]
mod tests_behavior_install_skills;
#[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 started = std::time::Instant::now();
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::BuildLease(args)) = cli.command {
commands::build_lease::run(args).await
}
if let Some(Command::Fleet { action }) = cli.command {
return commands::fleet::run(action).await;
}
if let Some(Command::Env { action }) = cli.command {
return commands::env_file::run(action);
}
match cli.command {
Some(Command::Secrets { action }) => return commands::secrets::run(action).await,
Some(Command::Content { action }) => return commands::content::run(action).await,
Some(Command::Worktrees(args)) => return commands::worktrees::run(args).await,
_ => {}
}
if let Some(Command::InternalSpawnDisclaimed {
launch_spec,
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);
}
if let Some(path) = launch_spec {
if let Err(e) = commands::spawn_disclaimed::run_launch_spec(&path) {
eprintln!("tm: {e:#}");
std::process::exit(1);
}
return Ok(());
}
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 (guard, store) = tracing_setup::init_daemon_tracing()?;
_daemon_log_guard = Some(guard);
_error_store = Some(store);
}
#[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(),
);
}
if commands::socket_dispatch::takes(&cli.command) {
return commands::socket_dispatch::run(cli).await;
}
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 account = cli.account.clone();
let account_token = if cli.account_token_stdin {
Some(commands::session_account::read_stdin_token(
std::io::stdin().lock(),
)?)
} else {
None
};
let token = account_token.as_deref();
let explicit_url = cli.url.as_deref();
let result = match cli.command {
None => commands::guided::run_guided_default(&client, &url, explicit_url).await,
Some(
Command::Status
| Command::Health
| Command::Doctor { .. }
| Command::Ticket { .. }
| Command::Pr { .. },
) => unreachable!("socket commands are dispatched before daemon-URL resolution"),
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 }) => {
let account = account.as_deref();
let daemon = DaemonClient::with_client(client.clone(), url.clone());
commands::session_account::session_as_account(&daemon, action, account, token).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();
let account = account.as_deref();
let daemon = DaemonClient::with_client(client.clone(), url.clone());
commands::session_account::session_as_account(&daemon, action, account, token).await
}
Some(Command::Events) => commands::misc::events(&client, &url).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::Tui {
url: tui_url,
interval_ms,
single_pane,
}) => {
let client = trusty_mpm::tui::client_for(tui_url.as_deref());
trusty_mpm::tui::run_initial_view_with(client, interval_ms, None, single_pane).await
}
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,
divert_check,
prompt_feedback,
}) => {
if pm_guard {
commands::pm_guard::pm_guard(&url, started).await
} else if divert_check {
commands::divert_check::divert_check().await
} else if prompt_feedback {
commands::prompt_feedback_hook::prompt_feedback_hook().await
} else {
hook(&client, &url).await
}
}
Some(Command::PromptFeedback(args)) => commands::prompt_feedback_cli::run(args).await,
Some(Command::Divert { action }) => commands::divert::run(action).await,
Some(Command::Memory { action }) => memory(action).await,
Some(Command::Compress { tool }) => run_compress(&tool).await,
Some(Command::Wait(args)) => commands::wait::run(args),
Some(Command::BuildLease(args)) => commands::build_lease::run(args).await,
Some(Command::Daemon {
addr,
tailscale,
mcp,
force,
sandbox,
}) => run_daemon(addr, tailscale, mcp, force, sandbox).await,
Some(Command::Supervisor {
interval,
auto_resume,
no_classify,
}) => commands::supervisor::run_supervisor(interval, auto_resume, no_classify).await,
Some(Command::Launch {
dir,
style,
worktree,
twin,
}) => {
if let Some(login) = account.as_deref() {
let target = commands::project::resolve_dir(dir.clone())?;
let daemon = DaemonClient::with_client(client.clone(), url.clone());
commands::session_account::pin_account_for_dir(&daemon, &target, login, token)
.await?;
}
let home = dirs::home_dir();
launch(
&client,
&url,
dir,
style,
worktree,
LaunchDir::OperatorCwd,
home.as_deref(),
twin, )
.await
}
Some(Command::Connect { dir }) => {
connect(&client, &url, dir, dirs::home_dir().as_deref()).await
}
Some(Command::Attach {
target,
json,
single_pane,
}) => attach_cmd(&client, &url, &target, json, single_pane, explicit_url).await,
Some(Command::Optimizer { action }) => optimizer(&client, &url, action).await,
Some(Command::Overseer { action }) => overseer(&client, &url, action).await,
Some(Command::Fleet { action }) => commands::fleet::run(action).await,
Some(Command::Env { action }) => commands::env_file::run(action),
Some(Command::Secrets { action }) => commands::secrets::run(action).await,
Some(Command::Content { action }) => commands::content::run(action).await,
Some(Command::Worktrees(args)) => commands::worktrees::run(args).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(&client, &url, action).await,
Some(Command::Auth { action }) => commands::auth::dispatch(action),
Some(Command::Catalog { action }) => commands::managed::catalog(action).await,
Some(Command::Issue { cmd, system }) => commands::issue::issue(cmd, system),
Some(Command::Watch { cmd }) => watch_dispatch::dispatch_watch(&client, &url, cmd).await,
Some(Command::Meta { action }) => commands::meta::meta(action).await,
Some(Command::Register {
url: repo_spec,
alias,
force,
root,
}) => {
let paths = commands::managed_root::resolve_managed_paths(root.as_deref())?;
commands::standalone::register_cmd(&paths, &repo_spec, alias.as_deref(), force)?;
let resolved_account = commands::register_args::resolve_account(
account.as_deref(),
commands::register_args::embedded_account(&repo_spec),
)?;
if let Some(resolved_account) = resolved_account {
let repo_url = commands::register_args::resolved_url(&repo_spec)?;
let name = trusty_mpm::project::derive_name_from_url(&repo_url)
.or_else(|| alias.clone())
.unwrap_or_else(|| repo_url.clone());
commands::projects::registry::auto_persist_account_selection(
&client,
&url,
name,
repo_url,
&resolved_account,
"registered",
)
.await;
}
Ok(())
}
Some(Command::Ls {
terms,
projects,
json,
source_id,
current,
all,
attached,
no_prune,
plain,
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, plain, sort, filter,
no_prune,
)
.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, account, token).await
}
Some(Command::External(ref tokens)) => {
let (external, argv) = (tokens.as_slice(), argv.as_slice());
commands::run_target::run_external(&client, &url, external, argv, &HELP, account, token)
.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::CommitTrailers(args)) => commands::commit_trailers::run(&args),
Some(Command::Banner { reconnecting }) => {
commands::banner::run_banner_preview(reconnecting)
}
Some(Command::Mcp { cmd }) => commands::mcp::dispatch(cmd).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
&& err
.downcast_ref::<trusty_mpm::core::DaemonUrlError>()
.is_some()
{
std::process::exit(trusty_mpm::core::exit_codes::EXIT_UNAVAILABLE);
}
result
}