Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions crates/auths-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn cli_styles() -> Styles {
about = "\x1b[1;32mauths \u{2014} cryptographic identity for developers and agents\x1b[0m",
version,
styles = cli_styles(),
after_help = "Run 'auths <command> --help' for details on any command.\nRun 'auths --help-all' for advanced commands (id, device, key, policy, ...)."
after_help = "Run 'auths <command> --help' for details on any command.\nRun 'auths --help-all' for all commands including advanced ones."
)]
pub struct AuthsCli {
#[command(subcommand)]
Expand All @@ -75,7 +75,7 @@ pub struct AuthsCli {
)]
pub format: OutputFormat,

#[clap(long, global = true, help = "Emit machine-readable JSON")]
#[clap(short = 'j', long, global = true, help = "Emit machine-readable JSON")]
pub json: bool,

#[clap(short, long, global = true, help = "Suppress non-essential output")]
Expand All @@ -93,35 +93,59 @@ pub struct AuthsCli {
#[derive(Subcommand, Debug)]
#[command(rename_all = "lowercase")]
pub enum RootCommand {
// ── Primary ──
Init(InitCommand),
Reset(ResetCommand),
Sign(SignCommand),
SignCommit(SignCommitCommand),
Verify(UnifiedVerifyCommand),
Status(StatusCommand),
Whoami(WhoamiCommand),
Tutorial(LearnCommand),

// ── Setup & Troubleshooting ──
Pair(PairCommand),
Doctor(DoctorCommand),
Tutorial(LearnCommand),

// ── Utilities ──
Config(ConfigCommand),
Completions(CompletionsCommand),

// ── Advanced (visible via --help-all) ──
#[command(hide = true)]
Reset(ResetCommand),
#[command(hide = true)]
SignCommit(SignCommitCommand),
#[command(hide = true)]
Signers(SignersCommand),
Pair(PairCommand),
#[command(hide = true)]
Error(ErrorLookupCommand),
Completions(CompletionsCommand),
#[command(hide = true)]
Emergency(EmergencyCommand),

Id(IdCommand),
#[command(hide = true)]
Device(DeviceCommand),
#[command(hide = true)]
Key(KeyCommand),
#[command(hide = true)]
Approval(ApprovalCommand),
#[command(hide = true)]
Artifact(ArtifactCommand),
#[command(hide = true)]
Policy(PolicyCommand),
#[command(hide = true)]
Git(GitCommand),
#[command(hide = true)]
Trust(TrustCommand),
#[command(hide = true)]
Namespace(NamespaceCommand),
#[command(hide = true)]
Org(OrgCommand),
#[command(hide = true)]
Audit(AuditCommand),
Config(ConfigCommand),
#[command(hide = true)]
Auth(AuthCommand),

// ── Internal (visible via --help-all) ──
#[command(hide = true)]
Emergency(EmergencyCommand),
#[command(hide = true)]
Agent(AgentCommand),
#[command(hide = true)]
Expand All @@ -136,5 +160,4 @@ pub enum RootCommand {
Log(LogCommand),
#[command(hide = true)]
Account(AccountCommand),
Auth(AuthCommand),
}
196 changes: 179 additions & 17 deletions crates/auths-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// CLI is the presentation boundary — printing and exit are expected here.
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)]
use anyhow::Result;
use clap::Parser;
use clap::{CommandFactory, Parser};

use auths_cli::cli::{AuthsCli, RootCommand};
use auths_cli::commands::executable::ExecutableCommand;
Expand Down Expand Up @@ -36,24 +36,25 @@ fn run() -> Result<()> {

let _telemetry = init_audit_sinks();

let cli = AuthsCli::parse();
// Intercept top-level help/--help-all BEFORE clap parsing so we can
// print grouped output while letting clap handle subcommand help normally
// (e.g. `auths init --help` still works via clap).
let raw_args: Vec<String> = std::env::args().skip(1).collect();
let has_help = raw_args.iter().any(|a| a == "--help" || a == "-h");
let has_help_all = raw_args.iter().any(|a| a == "--help-all");
let first_non_flag = raw_args.iter().find(|a| !a.starts_with('-'));

if cli.help_all {
use clap::CommandFactory;
let mut cmd = AuthsCli::command();
let sub_names: Vec<String> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
for name in &sub_names {
if let Some(sub) = cmd.find_subcommand_mut(name) {
*sub = sub.clone().hide(false);
}
}
cmd.print_help()?;
if has_help_all {
print_grouped_help(true)?;
return Ok(());
}
if has_help && first_non_flag.is_none() {
print_grouped_help(false)?;
return Ok(());
}

let cli = AuthsCli::parse();

let is_json = cli.json || matches!(cli.format, OutputFormat::Json);
if is_json {
set_json_mode(true);
Expand All @@ -64,8 +65,7 @@ fn run() -> Result<()> {
let command = match cli.command {
Some(cmd) => cmd,
None => {
use clap::CommandFactory;
AuthsCli::command().print_help()?;
print_grouped_help(false)?;
return Ok(());
}
};
Expand Down Expand Up @@ -119,3 +119,165 @@ fn run() -> Result<()> {

result
}

/// Command group definition for grouped help output.
struct CommandGroup {
heading: &'static str,
commands: &'static [&'static str],
}

/// The primary commands shown in default help.
const HELP_GROUPS: &[CommandGroup] = &[
CommandGroup {
heading: "Primary",
commands: &["init", "sign", "verify", "status", "whoami"],
},
CommandGroup {
heading: "Setup & Troubleshooting",
commands: &["pair", "doctor", "tutorial"],
},
CommandGroup {
heading: "Utilities",
commands: &["config", "completions"],
},
];

fn print_grouped_help(show_all: bool) -> Result<()> {
let cmd = AuthsCli::command();

// ANSI codes matching cli_styles()
const BLUE_BOLD: &str = "\x1b[1;34m";
const CYAN_BOLD: &str = "\x1b[1;36m";
const GREEN_BOLD: &str = "\x1b[1;32m";
const RESET: &str = "\x1b[0m";

// Header
println!("{GREEN_BOLD}auths \u{2014} cryptographic identity for developers and agents{RESET}");
println!();
println!("{BLUE_BOLD}Usage:{RESET} auths [OPTIONS] [COMMAND]");

// Collect subcommand metadata
let subcommands: Vec<(&str, String, bool)> = cmd
.get_subcommands()
.map(|s| {
let name = s.get_name();
let about = s.get_about().map(|a| a.to_string()).unwrap_or_default();
let hidden = s.is_hide_set();
(name, about, hidden)
})
.collect();

// Print primary groups
for group in HELP_GROUPS {
println!();
println!("{BLUE_BOLD}{}:{RESET}", group.heading);
for &cmd_name in group.commands {
if let Some((_, about, _)) = subcommands.iter().find(|(n, _, _)| *n == cmd_name) {
println!(" {CYAN_BOLD}{:<13}{RESET}{}", cmd_name, about);
}
}
}

// If --help-all, show advanced and internal groups
if show_all {
// Collect all names in primary groups
let primary_names: Vec<&str> = HELP_GROUPS
.iter()
.flat_map(|g| g.commands.iter().copied())
.collect();

// Internal commands (always hidden, even in --help-all context)
let internal = [
"emergency",
"agent",
"witness",
"scim",
"commit",
"debug",
"log",
"account",
];

// Advanced = hidden but not internal
let advanced: Vec<&(&str, String, bool)> = subcommands
.iter()
.filter(|(name, _, _)| !primary_names.contains(name) && !internal.contains(name))
.collect();

if !advanced.is_empty() {
println!();
println!("{BLUE_BOLD}Advanced:{RESET}");
for (name, about, _) in &advanced {
println!(" {CYAN_BOLD}{:<13}{RESET}{}", name, about);
}
}

// Internal
let internal_cmds: Vec<&(&str, String, bool)> = subcommands
.iter()
.filter(|(name, _, _)| internal.contains(name))
.collect();

if !internal_cmds.is_empty() {
println!();
println!("{BLUE_BOLD}Internal:{RESET}");
for (name, about, _) in &internal_cmds {
println!(" {CYAN_BOLD}{:<13}{RESET}{}", name, about);
}
}
}

// Options
println!();
println!("{BLUE_BOLD}Options:{RESET}");
for arg in cmd.get_arguments() {
if arg.is_hide_set() {
continue;
}
let long = arg.get_long().map(|l| format!("--{l}"));
let short = arg.get_short().map(|s| format!("-{s}"));
let about = arg.get_help().map(|h| h.to_string()).unwrap_or_default();

// Only show value placeholder for args that take a value (not bool flags)
let takes_value = !matches!(
arg.get_action(),
clap::ArgAction::SetTrue
| clap::ArgAction::SetFalse
| clap::ArgAction::Count
| clap::ArgAction::Version
| clap::ArgAction::Help
| clap::ArgAction::HelpShort
| clap::ArgAction::HelpLong
);
let value_hint = if takes_value {
arg.get_value_names()
.unwrap_or_default()
.iter()
.map(|v| format!(" <{v}>"))
.collect::<String>()
} else {
String::new()
};

// Build the flag string and pad to consistent width
let flag_str = match (short, long) {
(Some(s), Some(l)) => format!(" {s}, {l}{value_hint}"),
(None, Some(l)) => format!(" {l}{value_hint}"),
(Some(s), None) => format!(" {s}{value_hint}"),
(None, None) => continue,
};
println!("{CYAN_BOLD}{:<23}{RESET} {about}", flag_str);
}
// Help and version flags (clap adds these separately, not in get_arguments)
println!("{CYAN_BOLD}{:<23}{RESET} Print help", " -h, --help");
println!("{CYAN_BOLD}{:<23}{RESET} Print version", " -V, --version");

// Footer
println!();
println!("Run 'auths <command> --help' for details on any command.");
if !show_all {
println!("Run 'auths --help-all' for all commands including advanced ones.");
}

Ok(())
}
Loading