use anyhow::{Context, Result};
use arcbox_constants::paths::{DOCKER_CLI_TOOLS, HostLayout, labels};
use clap::Subcommand;
use super::OutputFormat;
#[derive(Subcommand)]
pub enum InternalCommands {
#[command(name = "brew-postflight")]
BrewPostflight,
#[command(name = "brew-uninstall")]
BrewUninstall,
}
pub async fn execute(cmd: InternalCommands) -> Result<()> {
match cmd {
InternalCommands::BrewPostflight => brew_postflight().await,
InternalCommands::BrewUninstall => brew_uninstall().await,
}
}
async fn brew_postflight() -> Result<()> {
let layout = HostLayout::resolve(None);
for dir in [
&layout.run_dir,
&layout.log_dir,
&layout.data_subdir,
&layout.data_dir.join("boot"),
&layout.data_dir.join("bin"),
] {
tokio::fs::create_dir_all(dir).await?;
}
super::setup::execute(super::setup::SetupCommands::Install, OutputFormat::Quiet).await?;
if let Err(e) = setup_docker_context() {
eprintln!("Note: Docker context setup skipped ({e})");
}
Ok(())
}
async fn brew_uninstall() -> Result<()> {
let layout = HostLayout::resolve(None);
let uid = unsafe { libc::getuid() };
let _ = std::process::Command::new("launchctl")
.args(["bootout", &format!("gui/{uid}/{}", labels::DAEMON)])
.output();
let _ = std::process::Command::new("pkill")
.args(["-f", labels::DAEMON])
.status();
std::thread::sleep(std::time::Duration::from_millis(200));
let plist_path = dirs::home_dir()
.context("could not determine home directory")?
.join(format!("Library/LaunchAgents/{}.plist", labels::DAEMON));
let _ = tokio::fs::remove_file(plist_path).await;
if let Ok(manager) = super::docker::context_manager() {
let _ = manager.remove_context();
}
super::setup::execute(super::setup::SetupCommands::Uninstall, OutputFormat::Quiet).await?;
if let Ok(client) = arcbox_helper::client::Client::connect().await {
for name in DOCKER_CLI_TOOLS {
let _ = client.cli_unlink(name).await;
}
}
let _ = tokio::fs::remove_dir_all(&layout.run_dir).await;
Ok(())
}
fn setup_docker_context() -> Result<()> {
let manager = super::docker::context_manager()?;
manager.enable().map_err(Into::into)
}
#[cfg(test)]
mod tests {
use arcbox_docker::DockerContextManager;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn postflight_enable_always_refreshes_socket_path() {
let temp = tempdir().unwrap();
let docker_dir = temp.path().join(".docker");
let old_socket = temp.path().join("old.sock");
let mgr = DockerContextManager::with_config_dir(old_socket, docker_dir.clone());
mgr.enable().unwrap();
assert!(mgr.context_exists());
assert!(mgr.is_default().unwrap());
let new_socket = temp.path().join("new.sock");
let mgr = DockerContextManager::with_config_dir(new_socket.clone(), docker_dir.clone());
mgr.enable().unwrap();
assert!(mgr.context_exists());
assert!(mgr.is_default().unwrap());
let meta_json = std::fs::read_dir(docker_dir.join("contexts/meta"))
.unwrap()
.filter_map(|e| e.ok())
.find_map(|e| std::fs::read_to_string(e.path().join("meta.json")).ok())
.expect("meta.json not found");
assert!(
meta_json.contains(&new_socket.to_string_lossy().to_string()),
"meta.json should reference new socket path, got: {meta_json}"
);
}
#[test]
fn uninstall_remove_context_restores_previous() {
let temp = tempdir().unwrap();
let socket = temp.path().join("docker.sock");
let docker_dir = temp.path().join(".docker");
let mgr = DockerContextManager::with_config_dir(socket, docker_dir);
std::fs::create_dir_all(mgr.docker_config_dir()).unwrap();
std::fs::write(
mgr.docker_config_dir().join("config.json"),
r#"{"currentContext":"desktop-linux"}"#,
)
.unwrap();
mgr.enable().unwrap();
assert!(mgr.is_default().unwrap());
mgr.remove_context().unwrap();
assert!(!mgr.context_exists());
assert_eq!(
mgr.current_context().unwrap(),
Some("desktop-linux".to_string())
);
}
#[test]
fn host_layout_directories_are_consistent() {
let layout = arcbox_constants::paths::HostLayout::resolve(None);
assert!(layout.run_dir.ends_with("run"));
assert!(layout.log_dir.ends_with("log"));
assert!(layout.data_subdir.ends_with("data"));
}
#[test]
fn remove_context_is_safe_when_docker_not_configured() {
let temp = tempdir().unwrap();
let socket = temp.path().join("docker.sock");
let docker_dir = temp.path().join(".docker");
let mgr = DockerContextManager::with_config_dir(socket, docker_dir);
mgr.remove_context().unwrap();
}
#[test]
fn context_manager_constructs_with_default_socket() {
let socket = arcbox_constants::paths::HostLayout::resolve(None).docker_socket;
assert!(socket.to_string_lossy().contains("docker.sock"));
}
#[test]
fn context_meta_accessible_via_public_api() {
let temp = tempdir().unwrap();
let mgr = DockerContextManager::with_config_dir(
PathBuf::from("/tmp/test.sock"),
temp.path().to_path_buf(),
);
assert!(!mgr.context_exists());
mgr.create_context().unwrap();
assert!(mgr.context_exists());
}
}