use std::path::{Path, PathBuf};
use crate::orchestrator::{absolute_program, ServiceState, ServiceUnit};
pub const TEAMS_ENTRY: &str = "bin/teams.mjs";
pub const TEAMS_PACKAGE: &str = "@volter-ai-dev/supercode-teams";
pub const SERVICE_DIR: &str = "service";
pub const SERVICE_NAME: &str = "dev.volter.supercode-teams-node";
#[derive(Debug, thiserror::Error)]
pub enum TeamsError {
#[error("no teams entry found (looked for `sdk/teams/{TEAMS_ENTRY}` under: {searched}); install it with `npm install -g {TEAMS_PACKAGE}`")]
NoEntry {
searched: String,
},
#[error("teams service: {action} failed: {detail}")]
Service {
action: &'static str,
detail: String,
},
#[error("teams file `{}`: {source}", path.display())]
File {
path: PathBuf,
source: std::io::Error,
},
}
pub fn teams_home() -> PathBuf {
if let Ok(home) = std::env::var("SUPERCODE_TEAMS_HOME") {
if !home.is_empty() {
return PathBuf::from(home);
}
}
crate::agent::global_instructions_dir().join("teams")
}
pub fn teams_entry() -> Result<PathBuf, TeamsError> {
let mut searched = Vec::new();
if let Some(explicit) = std::env::var_os("SUPERCODE_TEAMS_ENTRY") {
let path = PathBuf::from(explicit);
if path.is_file() {
return Ok(path);
}
searched.push(path.display().to_string());
}
let mut roots: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
}
if let Ok(cwd) = std::env::current_dir() {
roots.push(cwd);
}
if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
roots.push(workspace.to_path_buf());
}
for root in roots {
let candidate = root.join("sdk/teams").join(TEAMS_ENTRY);
if candidate.is_file() {
return Ok(candidate);
}
searched.push(candidate.display().to_string());
}
if let Some(global) = global_npm_root() {
let candidate = global.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
if candidate.is_file() {
return Ok(candidate);
}
searched.push(candidate.display().to_string());
}
Err(TeamsError::NoEntry {
searched: searched.join(", "),
})
}
fn global_npm_root() -> Option<PathBuf> {
let output = std::process::Command::new("npm")
.args(["root", "-g"])
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let root = text.trim();
if root.is_empty() {
return None;
}
Some(PathBuf::from(root))
}
pub fn service_unit(home: &Path, entry: &Path, node: &str) -> ServiceUnit {
let home_display = home.display().to_string();
let entry_display = entry.display().to_string();
if cfg!(target_os = "macos") {
let path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
let text = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>{SERVICE_NAME}</string>
<key>ProgramArguments</key>
<array>
<string>{node}</string>
<string>{entry_display}</string>
<string>node</string>
<string>start</string>
<string>--listen</string>
<string>127.0.0.1:0</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>SUPERCODE_TEAMS_HOME</key><string>{home_display}</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>{home_display}/service/teams-node.out.log</string>
<key>StandardErrorPath</key><string>{home_display}/service/teams-node.err.log</string>
</dict>
</plist>
"#
);
let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
ServiceUnit {
kind: "launchd",
path,
text,
install_command: install,
}
} else {
let path = home
.join(SERVICE_DIR)
.join(format!("{SERVICE_NAME}.service"));
let text = format!(
"[Unit]\n\
Description=supercode teams node ({home_display})\n\
After=network.target\n\
\n\
[Service]\n\
Environment=SUPERCODE_TEAMS_HOME={home_display}\n\
ExecStart={node} {entry_display} node start --listen 127.0.0.1:0\n\
Restart=on-failure\n\
KillSignal=SIGTERM\n\
\n\
[Install]\n\
WantedBy=default.target\n"
);
let install = format!(
"systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
path.display()
);
ServiceUnit {
kind: "systemd",
path,
text,
install_command: install,
}
}
}
pub fn write_unit(unit: &ServiceUnit) -> Result<(), TeamsError> {
if let Some(parent) = unit.path.parent() {
std::fs::create_dir_all(parent).map_err(|source| TeamsError::File {
path: unit.path.clone(),
source,
})?;
}
std::fs::write(&unit.path, &unit.text).map_err(|source| TeamsError::File {
path: unit.path.clone(),
source,
})
}
fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
let output = std::process::Command::new(program).args(args).output()?;
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&output.stderr));
Ok((output.status.success(), text.trim().to_string()))
}
#[cfg(target_os = "macos")]
fn gui_domain() -> String {
format!("gui/{}", unsafe { libc::getuid() })
}
pub fn service_status() -> ServiceState {
platform_status()
}
#[cfg(target_os = "macos")]
fn platform_status() -> ServiceState {
let label = SERVICE_NAME.to_string();
let target = format!("{}/{SERVICE_NAME}", gui_domain());
match run_tool("launchctl", &["print", &target]) {
Ok((true, text)) => ServiceState {
kind: "launchd",
label,
installed: true,
pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
},
Ok((false, _)) => ServiceState {
kind: "launchd",
label,
installed: false,
pid: None,
detail: format!("not bootstrapped in {}", gui_domain()),
},
Err(error) => ServiceState {
kind: "launchd",
label,
installed: false,
pid: None,
detail: format!("launchctl unavailable: {error}"),
},
}
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_status() -> ServiceState {
let label = SERVICE_NAME.to_string();
match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
Ok((active, text)) => {
let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
.map(|(ok, _)| ok)
.unwrap_or(false);
ServiceState {
kind: "systemd",
label,
installed: active || known,
pid: None,
detail: if text.is_empty() {
"unknown".into()
} else {
text
},
}
}
Err(error) => ServiceState {
kind: "systemd",
label,
installed: false,
pid: None,
detail: format!("systemctl unavailable: {error}"),
},
}
}
#[cfg(not(unix))]
fn platform_status() -> ServiceState {
ServiceState {
kind: "none",
label: SERVICE_NAME.to_string(),
installed: false,
pid: None,
detail: "no service manager on this platform".into(),
}
}
#[cfg(target_os = "macos")]
fn field_of(text: &str, key: &str) -> Option<String> {
text.lines()
.find_map(|line| line.trim().strip_prefix(key))
.map(|value| value.trim().to_string())
}
fn unit_file_name() -> String {
if cfg!(target_os = "macos") {
format!("{SERVICE_NAME}.plist")
} else {
format!("{SERVICE_NAME}.service")
}
}
pub fn install_service(
home: &Path,
entry: &Path,
node: &str,
) -> Result<(ServiceUnit, ServiceState), TeamsError> {
let existing = service_status();
if existing.installed {
return Err(TeamsError::Service {
action: "install",
detail: format!(
"`{}` is already installed ({}); `supercode teams node uninstall` first",
existing.label, existing.detail
),
});
}
let unit = service_unit(home, entry, &absolute_program(node));
write_unit(&unit)?;
platform_install(&unit)?;
Ok((unit, service_status()))
}
#[cfg(target_os = "macos")]
fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
let path = unit.path.display().to_string();
let (ok, text) =
run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
TeamsError::Service {
action: "install",
detail: format!("launchctl: {error}"),
}
})?;
if !ok {
return Err(TeamsError::Service {
action: "install",
detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
});
}
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
let path = unit.path.display().to_string();
for args in [
vec!["--user", "link", path.as_str()],
vec!["--user", "enable", "--now", SERVICE_NAME],
] {
let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
action: "install",
detail: format!("systemctl: {error}"),
})?;
if !ok {
return Err(TeamsError::Service {
action: "install",
detail: format!("systemctl {}: {text}", args.join(" ")),
});
}
}
Ok(())
}
#[cfg(not(unix))]
fn platform_install(_unit: &ServiceUnit) -> Result<(), TeamsError> {
Err(TeamsError::Service {
action: "install",
detail: "no service manager on this platform".into(),
})
}
pub fn uninstall_service(home: &Path) -> Result<ServiceState, TeamsError> {
platform_uninstall()?;
let unit_path = home.join(SERVICE_DIR).join(unit_file_name());
match std::fs::remove_file(&unit_path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(TeamsError::File {
path: unit_path,
source,
})
}
}
let mut state = service_status();
for _ in 0..40 {
if !state.installed {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
state = service_status();
}
Ok(state)
}
#[cfg(target_os = "macos")]
fn platform_uninstall() -> Result<(), TeamsError> {
let target = format!("{}/{SERVICE_NAME}", gui_domain());
let (ok, text) =
run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
action: "uninstall",
detail: format!("launchctl: {error}"),
})?;
if !ok && !text.contains("No such process") && !text.contains("not find") {
return Err(TeamsError::Service {
action: "uninstall",
detail: format!("launchctl bootout {target}: {text}"),
});
}
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_uninstall() -> Result<(), TeamsError> {
let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
Ok(())
}
#[cfg(not(unix))]
fn platform_uninstall() -> Result<(), TeamsError> {
Ok(())
}