use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use arcbox_docker::DockerContextManager;
use arcbox_docker_tools::{DockerToolManager, parse_tools};
use clap::Subcommand;
const LOCK_TOML: &str = include_str!("../../../../assets.lock");
#[derive(Subcommand)]
pub enum DockerCommands {
Enable,
Disable,
Status,
Setup,
}
pub async fn execute(cmd: DockerCommands) -> Result<()> {
match cmd {
DockerCommands::Enable => {
let manager = context_manager()?;
execute_enable(&manager)
}
DockerCommands::Disable => {
let manager = context_manager()?;
execute_disable(&manager)
}
DockerCommands::Status => {
let manager = context_manager()?;
execute_status(&manager);
Ok(())
}
DockerCommands::Setup => execute_setup().await,
}
}
fn context_manager() -> Result<DockerContextManager> {
DockerContextManager::new(default_socket_path())
.context("Failed to initialize Docker context manager")
}
async fn execute_setup() -> Result<()> {
let home = dirs::home_dir().context("could not determine home directory")?;
let runtime_bin = home.join(".arcbox/runtime/bin");
let user_bin = home.join(".arcbox/bin");
let tools = parse_tools(LOCK_TOML).context("failed to parse assets.lock")?;
if tools.is_empty() {
println!("No Docker tools configured in assets.lock.");
return Ok(());
}
let arch = arcbox_asset::current_arch().to_string();
println!("Installing Docker CLI tools...");
println!();
let manager = DockerToolManager::new(tools, &arch, runtime_bin.clone());
let progress_cb: arcbox_asset::ProgressCallback =
Box::new(|p: arcbox_asset::PrepareProgress| match &p.phase {
arcbox_asset::PreparePhase::Checking => {
eprint!(" [{}/{}] {} checking...", p.current, p.total, p.name);
}
arcbox_asset::PreparePhase::Downloading { downloaded, total } => {
let pct = total
.map(|t| if t > 0 { downloaded * 100 / t } else { 0 })
.unwrap_or(0);
eprint!(
"\r [{}/{}] {} downloading... {}%",
p.current, p.total, p.name, pct
);
}
arcbox_asset::PreparePhase::Verifying => {
eprint!(
"\r [{}/{}] {} verifying... ",
p.current, p.total, p.name
);
}
arcbox_asset::PreparePhase::Ready => {
eprintln!(
"\r [{}/{}] {} installed ",
p.current, p.total, p.name
);
}
arcbox_asset::PreparePhase::Cached => {
eprintln!(
"\r [{}/{}] {} up to date ",
p.current, p.total, p.name
);
}
});
manager
.install_all(Some(&Arc::new(progress_cb)))
.await
.context("failed to install Docker tools")?;
tokio::fs::create_dir_all(&user_bin).await?;
for tool in manager.tools() {
let target = runtime_bin.join(&tool.name);
let link = user_bin.join(&tool.name);
create_or_update_symlink(&target, &link).await?;
}
println!();
println!("Docker tools installed to {}", runtime_bin.display());
println!("Symlinks created in {}", user_bin.display());
generate_docker_completions(&home, &runtime_bin).await?;
println!();
println!("Restart your shell or re-source your profile to use Docker completions.");
Ok(())
}
async fn generate_docker_completions(home: &Path, runtime_bin: &Path) -> Result<()> {
let comp_dir = home.join(".arcbox/completions");
let docker_bin = runtime_bin.join("docker");
if !docker_bin.exists() {
return Ok(());
}
println!("Generating Docker shell completions...");
let shells = [
("zsh", comp_dir.join("zsh/_docker")),
("bash", comp_dir.join("bash/docker")),
("fish", comp_dir.join("fish/docker.fish")),
];
for (shell, dest) in &shells {
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let output = tokio::process::Command::new(&docker_bin)
.arg("completion")
.arg(shell)
.output()
.await;
match output {
Ok(out) if out.status.success() => {
tokio::fs::write(dest, &out.stdout).await?;
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
eprintln!(" Warning: docker completion {shell} failed: {stderr}");
}
Err(e) => {
eprintln!(" Warning: could not run docker completion: {e}");
}
}
}
let compose_bin = runtime_bin.join("docker-compose");
if compose_bin.exists() {
let compose_shells = [
("zsh", comp_dir.join("zsh/_docker-compose")),
("bash", comp_dir.join("bash/docker-compose")),
("fish", comp_dir.join("fish/docker-compose.fish")),
];
for (shell, dest) in &compose_shells {
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let output = tokio::process::Command::new(&compose_bin)
.arg("completion")
.arg(shell)
.output()
.await;
if let Ok(out) = output {
if out.status.success() {
tokio::fs::write(dest, &out.stdout).await?;
}
}
}
}
println!(" Completions saved to {}", comp_dir.display());
Ok(())
}
async fn create_or_update_symlink(target: &Path, link: &Path) -> Result<()> {
if tokio::fs::symlink_metadata(link).await.is_ok() {
tokio::fs::remove_file(link).await.ok();
}
#[cfg(unix)]
tokio::fs::symlink(target, link).await.with_context(|| {
format!(
"failed to create symlink {} -> {}",
link.display(),
target.display()
)
})?;
Ok(())
}
fn execute_enable(manager: &DockerContextManager) -> Result<()> {
if manager.context_exists() && manager.is_default()? {
println!("Docker integration is already enabled.");
return Ok(());
}
manager
.enable()
.context("Failed to enable Docker integration")?;
println!("Docker integration enabled.");
println!();
println!("You can now use the docker CLI with ArcBox:");
println!(" docker ps");
println!(" docker run alpine echo hello");
println!();
println!("To disable, run: arcbox docker disable");
if !manager.socket_path().exists() {
println!();
println!(
"Warning: ArcBox Docker socket not found at {}",
manager.socket_path().display()
);
println!("Make sure the ArcBox daemon is running.");
}
Ok(())
}
fn execute_disable(manager: &DockerContextManager) -> Result<()> {
if !manager.is_default()? {
println!("Docker integration is not currently enabled.");
return Ok(());
}
manager
.disable()
.context("Failed to disable Docker integration")?;
println!("Docker integration disabled.");
println!("The previous default Docker context has been restored.");
Ok(())
}
fn execute_status(manager: &DockerContextManager) {
let status = manager.status();
println!("Docker Integration Status");
println!("=========================");
println!();
println!(
"Context exists: {}",
if status.context_exists { "yes" } else { "no" }
);
println!(
"Is default: {}",
if status.is_default { "yes" } else { "no" }
);
println!("Socket path: {}", status.socket_path.display());
println!(
"Socket exists: {}",
if status.socket_exists { "yes" } else { "no" }
);
println!();
if status.is_default && status.socket_exists {
println!("Status: Ready - docker commands will use ArcBox");
} else if status.is_default && !status.socket_exists {
println!("Status: Enabled but daemon not running");
println!(" Start the ArcBox daemon to use docker commands");
} else if status.context_exists {
println!("Status: Context exists but not default");
println!(" Run 'arcbox docker enable' to activate");
} else {
println!("Status: Not configured");
println!(" Run 'arcbox docker enable' to set up");
}
}
fn default_socket_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".arcbox")
.join("docker.sock")
}