use anyhow::Context;
use super::managed_root::ManagedPaths;
pub(crate) fn register_cmd(
paths: &ManagedPaths,
alias: &str,
url: &str,
force: bool,
) -> anyhow::Result<()> {
let root = &paths.root;
let mut registry = trusty_mpm::core::standalone::registry::ManagedRegistry::load(root)
.with_context(|| format!("failed to load registry from {}", root.display()))?;
registry
.add(alias, url, force)
.with_context(|| format!("failed to register alias '{alias}'"))?;
registry.save().context("failed to save registry")?;
println!("registered {alias} → {url}");
Ok(())
}
pub(crate) fn ls_cmd(paths: &ManagedPaths, json: bool) -> anyhow::Result<()> {
let root = &paths.root;
let local_entries = trusty_mpm::core::project_aliases::ProjectAliasStore::load(root)
.unwrap_or_default()
.list();
let registry = trusty_mpm::core::standalone::registry::ManagedRegistry::load(root)
.with_context(|| format!("failed to load registry from {}", root.display()))?;
let fleet_entries = registry.list();
if json {
let local_rows: Vec<serde_json::Value> = local_entries
.iter()
.map(|e| {
serde_json::json!({
"alias": e.alias,
"path": e.path,
})
})
.collect();
let fleet_rows: Vec<serde_json::Value> = fleet_entries
.iter()
.map(|e| {
serde_json::json!({
"alias": e.alias,
"url": e.url,
"ref": e.git_ref,
"loaded": registry.is_loaded(&e.alias, root),
"repo_path": root.join("projects").join(&e.alias).join("repo"),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"local_projects": local_rows,
"managed_fleet": fleet_rows,
}))?
);
return Ok(());
}
if local_entries.is_empty() && fleet_entries.is_empty() {
println!("No aliases registered.");
println!(" Run `tm` from a git project to auto-register a local alias.");
println!(" Run `tm register <alias> <url>` to add a managed fleet alias.");
return Ok(());
}
if !local_entries.is_empty() {
println!("Local project aliases ({}):", local_entries.len());
let alias_w = local_entries
.iter()
.map(|e| e.alias.len())
.max()
.unwrap_or(5)
.max(5);
println!(" {:<alias_w$} PATH", "ALIAS");
println!(" {} {}", "─".repeat(alias_w), "─".repeat(40));
for e in &local_entries {
println!(" {:<alias_w$} {}", e.alias, e.path.display());
}
}
if !fleet_entries.is_empty() {
if !local_entries.is_empty() {
println!();
}
println!("Managed fleet aliases ({}):", fleet_entries.len());
let alias_w = fleet_entries
.iter()
.map(|e| e.alias.len())
.max()
.unwrap_or(5)
.max(5);
println!(" {:<alias_w$} {:<10} URL", "ALIAS", "LOADED");
println!(
" {} {} {}",
"─".repeat(alias_w),
"─".repeat(10),
"─".repeat(30)
);
for e in &fleet_entries {
let loaded = if registry.is_loaded(&e.alias, root) {
"yes"
} else {
"no"
};
println!(" {:<alias_w$} {:<10} {}", e.alias, loaded, e.url);
}
}
Ok(())
}
pub(crate) fn load_cmd(paths: &ManagedPaths, alias: &str) -> anyhow::Result<()> {
let root = &paths.root;
let cfg_dir = &paths.claude_config_dir;
trusty_mpm::core::standalone::global_config::ensure_global_config_dir(root, cfg_dir)?;
let repo_path = trusty_mpm::core::standalone::load::load_alias(alias, root, cfg_dir)
.with_context(|| format!("failed to load alias '{alias}'"))?;
println!("{}", repo_path.display());
Ok(())
}
pub(crate) fn run_cmd(paths: &ManagedPaths, alias: &str) -> anyhow::Result<()> {
let root = &paths.root;
let cfg_dir = &paths.claude_config_dir;
trusty_mpm::core::standalone::global_config::ensure_global_config_dir(root, cfg_dir)?;
trusty_mpm::core::standalone::run::run_alias(alias, root, cfg_dir)
.with_context(|| format!("failed to run alias '{alias}'"))
}
pub(crate) fn path_cmd(paths: &ManagedPaths, alias: &str) -> anyhow::Result<()> {
let root = &paths.root;
let repo = trusty_mpm::core::standalone::run::resolve_repo_path(alias, root)
.with_context(|| format!("failed to resolve path for alias '{alias}'"))?;
println!("{}", repo.display());
Ok(())
}
pub(crate) fn rm_cmd(paths: &ManagedPaths, alias: &str) -> anyhow::Result<()> {
let root = &paths.root;
let mut registry = trusty_mpm::core::standalone::registry::ManagedRegistry::load(root)
.with_context(|| format!("failed to load registry from {}", root.display()))?;
registry
.remove(alias)
.with_context(|| format!("alias '{alias}' is not registered"))?;
registry
.save()
.context("failed to save registry after rm")?;
let project_dir = root.join("projects").join(alias);
if project_dir.exists() {
std::fs::remove_dir_all(&project_dir).with_context(|| {
format!(
"failed to remove project directory {}",
project_dir.display()
)
})?;
}
println!(
"removed {alias} (registry entry deleted; project dir: {})",
project_dir.display()
);
Ok(())
}
pub(crate) fn update_cmd(paths: &ManagedPaths, alias: Option<&str>) -> anyhow::Result<()> {
let root = &paths.root;
let cfg_dir = &paths.claude_config_dir;
trusty_mpm::core::standalone::global_config::ensure_global_config_dir(root, cfg_dir)?;
let registry = trusty_mpm::core::standalone::registry::ManagedRegistry::load(root)
.with_context(|| format!("failed to load registry from {}", root.display()))?;
if let Some(a) = alias {
registry
.get(a)
.with_context(|| format!("alias '{a}' is not registered"))?;
let repo_dir = root.join("projects").join(a).join("repo");
if !repo_dir.exists() {
anyhow::bail!(
"alias '{a}' is registered but has not been loaded yet; \
run `tm load {a}` to clone and configure it first"
);
}
trusty_mpm::core::standalone::load::load_alias(a, root, cfg_dir)
.with_context(|| format!("failed to update alias '{a}'"))?;
println!("updated {a}");
return Ok(());
}
let targets: Vec<String> = registry
.list()
.into_iter()
.filter(|e| root.join("projects").join(&e.alias).join("repo").exists())
.map(|e| e.alias)
.collect();
if targets.is_empty() {
println!("no loaded aliases found — nothing to update");
return Ok(());
}
let mut failed: Vec<String> = Vec::new();
for target in &targets {
match trusty_mpm::core::standalone::load::load_alias(target, root, cfg_dir) {
Ok(_) => println!("updated {target}"),
Err(e) => {
eprintln!("error updating '{target}': {e}");
failed.push(target.clone());
}
}
}
if !failed.is_empty() {
anyhow::bail!("failed to update: {}", failed.join(", "));
}
Ok(())
}
pub(crate) fn login_cmd(paths: &ManagedPaths) -> anyhow::Result<()> {
let root = &paths.root;
let cfg_dir = &paths.claude_config_dir;
trusty_mpm::core::standalone::global_config::ensure_global_config_dir(root, cfg_dir)?;
eprintln!(
"tm login: one-time setup — authenticates managed `tm run` sessions\n\
on your Claude Max/Pro plan via the macOS Keychain.\n\
Config dir: {}\n\
Launching `claude auth login` — complete the browser OAuth flow...",
cfg_dir.display()
);
let status = trusty_mpm::core::spawn_disclaim::disclaimed_status(
&mut trusty_mpm::core::standalone::run::build_login_command(cfg_dir),
)
.context("failed to spawn 'claude auth login'; is `claude` installed and on PATH?")?;
if status.success() {
eprintln!(
"tm login: authentication complete.\n\
You can now run `tm run <alias>` — sessions will authenticate\n\
on your Claude plan automatically (no ANTHROPIC_API_KEY needed)."
);
Ok(())
} else {
eprintln!(
"tm login: `claude auth login` exited with {status}.\n\
If the OAuth flow was cancelled, run `tm login` again to retry.\n\
Alternatively, export ANTHROPIC_API_KEY to use the API-key path instead."
);
anyhow::bail!("claude auth login failed: {status}")
}
}