use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
pub fn resolve_grpc_socket_path() -> PathBuf {
if let Ok(path) = std::env::var("ARCBOX_GRPC_SOCKET") {
return PathBuf::from(path);
}
if let Ok(path) = std::env::var("ARCBOX_SOCKET") {
let docker_socket = PathBuf::from(path);
if let Some(parent) = docker_socket.parent() {
let preferred = parent.join("arcbox-grpc.sock");
if preferred.exists() {
return preferred;
}
let legacy = parent.join("arcbox.sock");
if legacy.exists() {
return legacy;
}
return preferred;
}
}
arcbox_constants::paths::HostLayout::from_env_or_default().grpc_socket
}
pub fn resolve_docker_socket_path() -> PathBuf {
let layout = arcbox_constants::paths::HostLayout::from_env_or_default();
select_docker_socket_path(
std::env::var_os("ARCBOX_SOCKET").map(PathBuf::from),
&layout,
)
}
fn select_docker_socket_path(
socket_override: Option<PathBuf>,
layout: &arcbox_constants::paths::HostLayout,
) -> PathBuf {
socket_override.unwrap_or_else(|| layout.docker_socket.clone())
}
pub mod agent;
pub mod boot;
pub mod cli_plugins;
pub mod daemon;
pub mod disk;
#[cfg(target_os = "macos")]
pub mod dns;
pub mod docker;
pub mod doctor;
#[cfg(target_os = "macos")]
pub mod install;
#[cfg(target_os = "macos")]
pub mod internal;
pub mod kubernetes;
pub mod logs;
pub mod machine;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod migrate;
pub mod sandbox;
pub mod setup;
pub mod symlink;
pub mod system;
pub mod top;
#[cfg(target_os = "macos")]
pub mod uninstall;
pub mod version;
#[derive(Parser)]
#[command(name = "abctl")]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub socket: Option<PathBuf>,
#[arg(long, global = true)]
pub profile: Option<arcbox_constants::paths::ArcboxProfile>,
#[arg(long, global = true, default_value = "table")]
pub format: OutputFormat,
#[arg(long, global = true)]
pub debug: bool,
}
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Table,
Json,
Quiet,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(subcommand)]
Machine(machine::MachineCommands),
#[cfg(target_os = "macos")]
#[command(subcommand)]
Macos(macos::MacosCommands),
#[command(subcommand)]
Migrate(migrate::MigrateCommands),
#[command(subcommand)]
Sandbox(sandbox::SandboxCommands),
Claude(agent::AgentArgs),
#[command(subcommand)]
Docker(docker::DockerCommands),
#[command(subcommand, alias = "k8s")]
Kubernetes(kubernetes::KubernetesCommands),
#[command(subcommand)]
System(system::SystemCommands),
#[command(subcommand)]
Boot(boot::BootCommands),
#[command(subcommand)]
Disk(disk::DiskCommands),
#[cfg(target_os = "macos")]
#[command(subcommand)]
Dns(dns::DnsCommands),
Daemon(daemon::DaemonArgs),
Logs(logs::LogsArgs),
#[command(subcommand)]
Setup(setup::SetupCommands),
Doctor,
Top(top::TopArgs),
#[cfg(target_os = "macos")]
#[command(name = "_install", hide = true)]
Install(install::InstallArgs),
#[cfg(target_os = "macos")]
#[command(name = "_uninstall", hide = true)]
Uninstall(uninstall::UninstallArgs),
#[cfg(target_os = "macos")]
#[command(name = "_internal", hide = true, subcommand)]
Internal(internal::InternalCommands),
Info,
Version,
}
impl Cli {
pub fn validate_output_format(&self) -> anyhow::Result<()> {
let supported = match self.format {
OutputFormat::Table => true,
OutputFormat::Json => match &self.command {
Commands::Boot(
boot::BootCommands::Prefetch(_)
| boot::BootCommands::Status(_)
| boot::BootCommands::Clear
| boot::BootCommands::List,
)
| Commands::Setup(
setup::SetupCommands::Install
| setup::SetupCommands::Uninstall
| setup::SetupCommands::Status,
)
| Commands::Docker(docker::DockerCommands::Setup)
| Commands::Top(_)
| Commands::Disk(disk::DiskCommands::Usage)
| Commands::Machine(machine::MachineCommands::Inspect(_))
| Commands::Sandbox(sandbox::SandboxCommands::Inspect(_))
| Commands::Doctor => true,
#[cfg(target_os = "macos")]
Commands::Dns(dns::DnsCommands::Status) => true,
_ => false,
},
OutputFormat::Quiet => matches!(
&self.command,
Commands::Setup(
setup::SetupCommands::Install
| setup::SetupCommands::Uninstall
| setup::SetupCommands::Status
)
),
};
if supported {
return Ok(());
}
let format = match self.format {
OutputFormat::Json => "json",
OutputFormat::Quiet => "quiet",
OutputFormat::Table => unreachable!("table output is always supported"),
};
anyhow::bail!("--format {format} is not supported for this command")
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use arcbox_constants::paths::HostLayout;
use clap::Parser;
use super::{Cli, select_docker_socket_path};
#[test]
fn docker_socket_uses_override_then_host_layout() {
let layout = HostLayout::new(PathBuf::from("custom-data"));
assert_eq!(
select_docker_socket_path(None, &layout),
layout.docker_socket
);
assert_eq!(
select_docker_socket_path(Some(PathBuf::from("override.sock")), &layout),
PathBuf::from("override.sock")
);
}
#[test]
fn query_output_format_matrix() {
let mut cases: Vec<(&[&str], bool, bool)> = vec![
(&["version"], false, false),
(&["info"], false, false),
(&["doctor"], true, false),
(&["top"], true, false),
(&["logs"], false, false),
(&["daemon", "status"], false, false),
(&["setup", "status"], true, true),
(&["setup", "completions", "--shell", "zsh"], false, false),
(&["disk", "usage"], true, false),
(&["boot", "status"], true, false),
(&["boot", "list"], true, false),
(&["system", "backend"], false, false),
(&["kubernetes", "status"], false, false),
(&["kubernetes", "kubeconfig"], false, false),
(&["docker", "status"], false, false),
(&["machine", "ls"], false, false),
(&["machine", "status", "default"], false, false),
(&["machine", "inspect", "default"], true, false),
(&["machine", "ping", "default"], false, false),
(&["machine", "info", "default"], false, false),
(&["sandbox", "ls"], false, false),
(&["sandbox", "inspect", "sandbox"], true, false),
(&["sandbox", "events"], false, false),
(&["sandbox", "snapshots"], false, false),
(&["sandbox", "presets"], false, false),
(
&["migrate", "from", "docker-desktop", "--dry-run"],
false,
false,
),
(&["migrate", "from", "orbstack", "--dry-run"], false, false),
];
#[cfg(target_os = "macos")]
cases.extend([
(&["dns", "status"][..], true, false),
(&["macos", "ls"][..], false, false),
(&["macos", "ip", "guest"][..], false, false),
(
&["macos", "image", "resolve", "tahoe-base"][..],
false,
false,
),
(&["macos", "image", "ls"][..], false, false),
]);
for (args, json, quiet) in cases {
assert!(accepts(args, "table"), "table rejected for {args:?}");
assert_eq!(accepts(args, "json"), json, "JSON mismatch for {args:?}");
assert_eq!(accepts(args, "quiet"), quiet, "quiet mismatch for {args:?}");
}
}
fn accepts(args: &[&str], format: &str) -> bool {
let argv: Vec<_> = ["abctl", "--format", format]
.into_iter()
.chain(args.iter().copied())
.collect();
Cli::try_parse_from(argv)
.expect("matrix command must parse")
.validate_output_format()
.is_ok()
}
}