use crate::cli::ProjectAction;
pub(crate) fn resolve_dir(dir: Option<String>) -> anyhow::Result<std::path::PathBuf> {
match dir {
Some(d) => Ok(std::path::PathBuf::from(d)),
None => Ok(std::env::current_dir()?),
}
}
pub(crate) async fn project(
client: &reqwest::Client,
url: &str,
action: ProjectAction,
) -> anyhow::Result<()> {
match action {
ProjectAction::Init { dir } => {
let path = resolve_dir(dir)?;
let body: serde_json::Value = client
.post(format!("{url}/projects"))
.json(&serde_json::json!({ "path": path }))
.send()
.await?
.error_for_status()?
.json()
.await?;
let report = scaffold_project_dir(&path)?;
for line in &report {
println!(" {line}");
}
let name = body.get("name").and_then(|v| v.as_str()).unwrap_or("?");
println!("registered project '{name}' at {}", path.display());
}
ProjectAction::List => {
for row in list_rows(client, url).await? {
println!("{row}");
}
}
ProjectAction::Info { dir } => {
let path = resolve_dir(dir)?;
let resp = client
.get(format!("{url}/projects/current"))
.query(&[("path", path.to_string_lossy().as_ref())])
.send()
.await?;
if resp.status() == reqwest::StatusCode::NOT_FOUND {
println!("{} is not a registered project", path.display());
} else {
let body: serde_json::Value = resp.error_for_status()?.json().await?;
println!("{}", serde_json::to_string_pretty(&body)?);
}
let trusted = trusty_mpm::core::project_trust::is_project_trusted(&path);
println!(
"project-scope custom MCP trust: {}",
if trusted { "trusted" } else { "untrusted" }
);
}
ProjectAction::Trust { dir, revoke } => trust_cmd(dir, revoke)?,
}
Ok(())
}
pub(crate) async fn list_rows(client: &reqwest::Client, url: &str) -> anyhow::Result<Vec<String>> {
let projects = trusty_mpm::client::DaemonClient::with_client(client.clone(), url.to_string())
.registry_list_projects(None)
.await?;
if projects.is_empty() {
return Ok(vec!["no projects registered".to_string()]);
}
let aliases = local_project_paths();
Ok(projects
.iter()
.map(|p| {
let trusted = aliases
.get(&p.name)
.is_some_and(|path| trusty_mpm::core::project_trust::is_project_trusted(path));
project_list_row(p, trusted)
})
.collect())
}
pub(crate) fn project_list_row(p: &trusty_mpm::project::Project, trusted: bool) -> String {
let marker = if trusted { " [mcp-trusted]" } else { "" };
format!(
"{}{marker}",
crate::commands::projects::registry::render_project_line(p)
)
}
fn local_project_paths() -> std::collections::HashMap<String, std::path::PathBuf> {
let root = trusty_mpm::core::paths::FrameworkPaths::default().root;
trusty_mpm::core::project_aliases::ProjectAliasStore::load(&root)
.unwrap_or_default()
.list()
.iter()
.map(|e| (e.alias.clone(), e.path.clone()))
.collect()
}
pub(crate) fn trust_cmd(dir: Option<String>, revoke: bool) -> anyhow::Result<()> {
let Some(root) = trusty_mpm::core::project_trust::trust_store_root() else {
anyhow::bail!("cannot resolve the home directory for the project-trust store");
};
trust_cmd_in(dir, revoke, &root)
}
pub(crate) fn trust_cmd_in(
dir: Option<String>,
revoke: bool,
root: &std::path::Path,
) -> anyhow::Result<()> {
let path = resolve_dir(dir)?;
let mut store = trusty_mpm::core::project_trust::ProjectTrustStore::load(root)?;
if revoke {
if store.revoke(&path) {
store.save()?;
println!("revoked trust for {}", path.display());
} else {
println!("{} was not trusted (no change)", path.display());
}
} else if store.trust(&path) {
store.save()?;
println!(
"trusted {} — its [mcp.custom] manifest entries will now be bridged into fleet \
sessions",
path.display()
);
} else {
println!("{} is already trusted (no change)", path.display());
}
Ok(())
}
pub(crate) fn scaffold_project_dir(project: &std::path::Path) -> anyhow::Result<Vec<String>> {
let mut report = Vec::new();
let dotdir = trusty_mpm::core::harness_root::harness_dir(project);
let sessions = dotdir.join("sessions");
std::fs::create_dir_all(&sessions)?;
report.push(format!("\u{2713} {}", sessions.display()));
let framework = dotdir.join(trusty_mpm::core::harness_root::FRAMEWORK_DIR);
std::fs::create_dir_all(&framework)?;
report.push(format!("\u{2713} {}", framework.display()));
let config = dotdir.join("config.toml");
if config.exists() {
report.push(format!("- {} (exists, skipped)", config.display()));
} else {
let name = trusty_mpm::core::project::name_from_path(project);
let contents = format!(
"# trusty-mpm project configuration\n\
# Generated by: trusty-mpm project init\n\n\
[project]\nname = \"{name}\"\n\n\
[agents]\n\
# Additional agent sources for this project\n\
# sources = [\"https://example.com/agents\"]\n\n\
[skills]\n\
# Additional skill sources for this project\n\
# sources = []\n"
);
std::fs::write(&config, contents)?;
report.push(format!("\u{2713} {}", config.display()));
}
Ok(report)
}