use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::Context;
use super::load::load_alias;
use super::registry::ManagedRegistry;
pub fn build_launch_command(
repo_path: &Path,
claude_config_dir: &Path,
api_key: Option<&str>,
) -> Command {
let mut cmd = Command::new("claude");
cmd.current_dir(repo_path);
cmd.env("CLAUDE_CONFIG_DIR", claude_config_dir);
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
cmd.arg(crate::core::model_inject::PERMISSION_MODE_FLAG);
if let Some(key) = api_key
&& !key.trim().is_empty()
{
cmd.arg("--bare");
}
cmd
}
pub fn build_login_command(claude_config_dir: &Path) -> Command {
let mut cmd = Command::new("claude");
cmd.args(["auth", "login"]);
cmd.env("CLAUDE_CONFIG_DIR", claude_config_dir);
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
cmd
}
#[derive(Debug, PartialEq, Eq)]
pub enum AuthState {
ApiKey,
CredentialsFile,
Unknown,
}
pub fn check_credentials(claude_config_dir: &Path, api_key: Option<&str>) -> AuthState {
if let Some(key) = api_key
&& !key.trim().is_empty()
{
return AuthState::ApiKey;
}
if claude_config_dir.join(".credentials.json").exists() {
return AuthState::CredentialsFile;
}
AuthState::Unknown
}
pub fn run_alias(alias: &str, managed_root: &Path, claude_config_dir: &Path) -> anyhow::Result<()> {
let repo_path = load_alias(alias, managed_root, claude_config_dir)
.with_context(|| format!("failed to load alias '{alias}'"))?;
let api_key = std::env::var("ANTHROPIC_API_KEY").ok();
match check_credentials(claude_config_dir, api_key.as_deref()) {
AuthState::ApiKey => {
}
AuthState::CredentialsFile => {
}
AuthState::Unknown => {
eprintln!(
"hint: no ANTHROPIC_API_KEY and no .credentials.json found in {}.\n\
If you haven't run `tm login` yet, do so once to authenticate\n\
managed sessions on your Claude Max/Pro plan.\n\
Alternatively, export ANTHROPIC_API_KEY to use the API-key path.",
claude_config_dir.display()
);
}
}
let mut cmd = build_launch_command(&repo_path, claude_config_dir, api_key.as_deref());
crate::core::spawn_disclaim::disclaimed_status(&mut cmd)
.context("failed to spawn 'claude'; is it installed and on PATH?")?;
Ok(())
}
pub fn resolve_repo_path(alias: &str, managed_root: &Path) -> anyhow::Result<PathBuf> {
let registry = ManagedRegistry::load(managed_root)
.with_context(|| format!("failed to load registry from {}", managed_root.display()))?;
registry
.get(alias)
.with_context(|| format!("alias '{alias}' is not registered"))?;
let repo = managed_root.join("projects").join(alias).join("repo");
let marker = repo.join(".trusty-mpm").join("managed.toml");
if !marker.exists() {
anyhow::bail!("alias '{alias}' is registered but not loaded; run `tm load {alias}` first");
}
Ok(repo)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_build_launch_command_sets_env_and_cwd() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
let cfg = tmp.path().join("claude-config");
let cmd = build_launch_command(&repo, &cfg, None);
assert_eq!(cmd.get_program(), "claude");
assert_eq!(cmd.get_current_dir(), Some(repo.as_path()));
let env_vars: Vec<_> = cmd.get_envs().collect();
let claude_cfg_var = env_vars
.iter()
.find(|(k, _)| *k == std::ffi::OsStr::new("CLAUDE_CONFIG_DIR"));
assert!(
claude_cfg_var.is_some(),
"CLAUDE_CONFIG_DIR should be set in the command env"
);
if let Some((_, val)) = claude_cfg_var {
assert_eq!(*val, Some(cfg.as_os_str()));
}
}
#[test]
fn test_build_launch_command_adds_bare_with_api_key() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
let cfg = tmp.path().join("claude-config");
let cmd = build_launch_command(&repo, &cfg, Some("sk-ant-test-key"));
let args: Vec<_> = cmd.get_args().collect();
assert!(
args.iter().any(|a| a == &std::ffi::OsStr::new("--bare")),
"expected --bare in args when api_key is set; got {args:?}"
);
}
#[test]
fn test_build_launch_command_no_bare_without_api_key() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
let cfg = tmp.path().join("claude-config");
let cmd = build_launch_command(&repo, &cfg, None);
let args: Vec<_> = cmd.get_args().collect();
assert!(
!args.iter().any(|a| a == &std::ffi::OsStr::new("--bare")),
"--bare must not be present when api_key is None; got {args:?}"
);
let cmd2 = build_launch_command(&repo, &cfg, Some(""));
let args2: Vec<_> = cmd2.get_args().collect();
assert!(
!args2.iter().any(|a| a == &std::ffi::OsStr::new("--bare")),
"--bare must not be present for empty api_key; got {args2:?}"
);
let cmd3 = build_launch_command(&repo, &cfg, Some(" "));
let args3: Vec<_> = cmd3.get_args().collect();
assert!(
!args3.iter().any(|a| a == &std::ffi::OsStr::new("--bare")),
"--bare must not be present for whitespace-only key; got {args3:?}"
);
}
#[test]
fn test_build_launch_command_includes_bypass_permissions() {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
let cfg = tmp.path().join("claude-config");
let cmd = build_launch_command(&repo, &cfg, None);
let args: Vec<_> = cmd.get_args().collect();
assert!(
args.iter()
.any(|a| *a == std::ffi::OsStr::new("--dangerously-skip-permissions")),
"expected --dangerously-skip-permissions in args (no key); got {args:?}"
);
let cmd2 = build_launch_command(&repo, &cfg, Some("sk-ant-test-key"));
let args2: Vec<_> = cmd2.get_args().collect();
assert!(
args2
.iter()
.any(|a| *a == std::ffi::OsStr::new("--dangerously-skip-permissions")),
"expected --dangerously-skip-permissions with api key; got {args2:?}"
);
assert!(
args2.iter().any(|a| *a == std::ffi::OsStr::new("--bare")),
"expected --bare to co-exist with bypass flag; got {args2:?}"
);
}
#[test]
fn test_build_login_command_sets_env_and_invocation() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().join("claude-config");
let cmd = build_login_command(&cfg);
assert_eq!(cmd.get_program(), "claude");
let args: Vec<_> = cmd.get_args().collect();
assert_eq!(
args,
vec![std::ffi::OsStr::new("auth"), std::ffi::OsStr::new("login")],
"expected [auth, login], got {args:?}"
);
let env_vars: Vec<_> = cmd.get_envs().collect();
let cfg_var = env_vars
.iter()
.find(|(k, _)| *k == std::ffi::OsStr::new("CLAUDE_CONFIG_DIR"));
assert!(
cfg_var.is_some(),
"CLAUDE_CONFIG_DIR must be set on the login command"
);
if let Some((_, val)) = cfg_var {
assert_eq!(*val, Some(cfg.as_os_str()));
}
}
#[test]
fn test_check_credentials_with_key_param() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().to_path_buf();
assert_eq!(
check_credentials(&cfg, Some("sk-test-key")),
AuthState::ApiKey
);
assert_eq!(check_credentials(&cfg, Some("")), AuthState::Unknown);
assert_eq!(check_credentials(&cfg, Some(" ")), AuthState::Unknown);
assert_eq!(check_credentials(&cfg, None), AuthState::Unknown);
}
#[test]
fn test_check_credentials_with_file() {
let tmp = TempDir::new().unwrap();
let cfg = tmp.path().to_path_buf();
std::fs::write(cfg.join(".credentials.json"), r#"{"token":"t"}"#).unwrap();
assert_eq!(check_credentials(&cfg, None), AuthState::CredentialsFile);
assert_eq!(
check_credentials(&cfg, Some("sk-also-valid")),
AuthState::ApiKey
);
}
}