use clap::{Command, CommandFactory, Parser, Subcommand};
use crate::commands::{
api, audit, auth, bench, build, changelog, changes, component, config, daemon, db, deploy,
deps, extension, file, fleet, git, issues, lint, logs, project, refactor, release, report,
review, rig, self_cmd, server, ssh, stack, status, test, trace, triage, undo, upgrade, version,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser)]
#[command(name = "homeboy")]
#[command(version = VERSION)]
#[command(about = "CLI tool for development and deployment automation")]
pub struct Cli {
#[arg(long, global = true, value_name = "PATH")]
pub output: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(visible_alias = "projects")]
Project(project::ProjectArgs),
Ssh(ssh::SshArgs),
#[command(visible_alias = "servers")]
Server(server::ServerArgs),
Test(test::TestArgs),
Bench(bench::BenchArgs),
Trace(trace::TraceArgs),
Lint(lint::LintArgs),
Db(db::DbArgs),
#[command(visible_alias = "dependencies")]
Deps(deps::DepsArgs),
File(file::FileArgs),
#[command(visible_alias = "fleets")]
Fleet(fleet::FleetArgs),
Logs(logs::LogsArgs),
Triage(triage::TriageArgs),
Deploy(deploy::DeployArgs),
#[command(visible_alias = "components")]
Component(component::ComponentArgs),
Config(config::ConfigArgs),
Daemon(daemon::DaemonArgs),
#[command(visible_alias = "extensions")]
Extension(extension::ExtensionArgs),
Status(status::StatusArgs),
Docs(crate::commands::docs::DocsArgs),
Changelog(changelog::ChangelogArgs),
Git(git::GitArgs),
Issues(issues::IssuesArgs),
Version(version::VersionArgs),
Build(build::BuildArgs),
Changes(changes::ChangesArgs),
Release(release::ReleaseArgs),
Report(report::ReportArgs),
Review(review::ReviewArgs),
Audit(audit::AuditArgs),
Refactor(refactor::RefactorArgs),
#[command(visible_alias = "rigs")]
Rig(rig::RigArgs),
#[command(name = "self")]
SelfCmd(self_cmd::SelfArgs),
#[command(visible_alias = "stacks")]
Stack(stack::StackArgs),
Undo(undo::UndoArgs),
Auth(auth::AuthArgs),
Api(api::ApiArgs),
Upgrade(upgrade::UpgradeArgs),
List,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandSurface {
pub commands: Vec<CommandSurfaceEntry>,
}
impl CommandSurface {
pub fn contains_path(&self, path: &[&str]) -> bool {
let Some((first, rest)) = path.split_first() else {
return false;
};
let Some(entry) = self.commands.iter().find(|entry| entry.matches(first)) else {
return false;
};
match rest {
[] => true,
[second] => entry.subcommands.iter().any(|sub| sub.matches(second)),
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandSurfaceEntry {
pub name: String,
pub visible_aliases: Vec<String>,
pub subcommands: Vec<CommandSurfaceEntry>,
}
impl CommandSurfaceEntry {
fn matches(&self, name: &str) -> bool {
self.name == name || self.visible_aliases.iter().any(|alias| alias == name)
}
}
pub fn current_command_surface() -> CommandSurface {
command_surface_from(Cli::command())
}
pub fn command_surface_from(command: Command) -> CommandSurface {
CommandSurface {
commands: visible_subcommands(&command, 1),
}
}
fn visible_subcommands(command: &Command, remaining_depth: usize) -> Vec<CommandSurfaceEntry> {
command
.get_subcommands()
.filter(|subcommand| !subcommand.is_hide_set())
.map(|subcommand| CommandSurfaceEntry {
name: subcommand.get_name().to_string(),
visible_aliases: subcommand
.get_visible_aliases()
.map(str::to_string)
.collect(),
subcommands: if remaining_depth == 0 {
Vec::new()
} else {
visible_subcommands(subcommand, remaining_depth - 1)
},
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_current_command_surface() {
let surface = current_command_surface();
assert!(surface.contains_path(&["self"]));
assert!(surface.contains_path(&["self", "status"]));
}
#[test]
fn test_command_surface_from() {
let surface = command_surface_from(Cli::command());
assert!(surface.contains_path(&["self"]));
assert!(surface.contains_path(&["self", "status"]));
}
#[test]
fn test_contains_path() {
let surface = current_command_surface();
assert!(surface.contains_path(&["self"]));
assert!(!surface.contains_path(&["self", "missing"]));
}
}