use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Value, json};
use crate::apps::{self, Env};
pub fn project_url(root: &Path) -> Result<String> {
let env = Env::load(root)?;
let host = env.get("APP_HOST").unwrap_or_else(|| "127.0.0.1".into());
let port = env
.get("APP_PORT")
.and_then(|port| port.parse().ok())
.unwrap_or(3000);
Ok(format!("http://{}/mcp", apps::connect_address(&host, port)))
}
pub fn login(url: &str) -> Result<()> {
let email = prompt("Email: ")?;
call(url, None, "auth_email_start", json!({ "email": email }))?;
let code = prompt(
"We sent you a 6-digit code (printed in the backend's log until SMTP_URL is set). Code: ",
)?;
let session = call(
url,
None,
"auth_email_verify",
json!({ "email": email, "code": code }),
)?;
let token = session["token"]
.as_str()
.ok_or_else(|| anyhow!("the backend returned no session token"))?;
let mut sessions = load()?;
sessions.insert(url.to_owned(), token.to_owned());
save(&sessions)?;
println!("Signed in to {url} as {email}.");
Ok(())
}
pub fn logout(url: &str) -> Result<()> {
let mut sessions = load()?;
if let Some(token) = sessions.remove(url) {
let _ = call(url, Some(&token), "auth_logout", json!({}));
save(&sessions)?;
}
println!("Signed out of {url}.");
Ok(())
}
pub fn remember_project(root: &Path) -> Result<()> {
let path = config_dir()?.join("project");
std::fs::create_dir_all(config_dir()?)?;
std::fs::write(path, root.display().to_string())?;
Ok(())
}
pub fn remembered_project() -> Option<PathBuf> {
let path = std::fs::read_to_string(config_dir().ok()?.join("project")).ok()?;
let root = PathBuf::from(path.trim());
root.join("Cargo.toml").is_file().then_some(root)
}
pub fn token(url: &str) -> Result<String> {
load()?.remove(url).ok_or_else(|| {
anyhow!("sign in first: `slave login` (with the backend running, `slave serve`)")
})
}
pub fn call(url: &str, token: Option<&str>, tool: &str, arguments: Value) -> Result<Value> {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": tool, "arguments": arguments },
});
let mut curl = Command::new("curl");
curl.args(["-sS", "--max-time", "30", "-X", "POST", url])
.args(["-H", "content-type: application/json"])
.args(["-H", "accept: application/json, text/event-stream"])
.args(["--data-binary", &body.to_string()]);
if let Some(token) = token {
curl.args(["-H", &format!("authorization: Bearer {token}")]);
}
let output = curl
.output()
.context("failed to run curl (is it installed?)")?;
if !output.status.success() {
bail!(
"cannot reach {url}: {} (is `slave serve` running?)",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let response: Value = serde_json::from_slice(&output.stdout)
.with_context(|| format!("unexpected answer from {url}"))?;
let result = &response["result"];
if result["isError"].as_bool() == Some(true) {
let content = &result["structuredContent"];
if content["code"] == "unauthenticated" {
bail!("your session ended; sign in again with `slave login`");
}
bail!("{}", content["error"].as_str().unwrap_or("the tool failed"));
}
if result.is_null() {
bail!(
"{}",
response["error"]["message"]
.as_str()
.unwrap_or("unexpected answer")
);
}
Ok(result["structuredContent"].clone())
}
pub fn prompt(question: &str) -> Result<String> {
print!("{question}");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(line.trim().to_owned())
}
fn config_dir() -> Result<PathBuf> {
let home = std::env::var_os("HOME").ok_or_else(|| anyhow!("HOME is not set"))?;
Ok(PathBuf::from(home).join(".config/slave"))
}
fn path() -> Result<PathBuf> {
Ok(config_dir()?.join("sessions.json"))
}
fn load() -> Result<BTreeMap<String, String>> {
match std::fs::read(path()?) {
Ok(bytes) => Ok(serde_json::from_slice(&bytes).unwrap_or_default()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(BTreeMap::new()),
Err(err) => Err(err.into()),
}
}
fn save(sessions: &BTreeMap<String, String>) -> Result<()> {
let path = path()?;
std::fs::create_dir_all(path.parent().expect("a parent directory"))?;
std::fs::write(&path, serde_json::to_vec_pretty(sessions)?)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
}
Ok(())
}