preparing for v1.12

This commit is contained in:
Mick Grove 2025-06-24 17:17:16 -07:00
commit fc4aee9e41
249 changed files with 121395 additions and 0 deletions

128
src/cli/commands/github.rs Normal file
View file

@ -0,0 +1,128 @@
use clap::{Args, Subcommand, ValueEnum, ValueHint};
use strum_macros::Display;
use url::Url;
use crate::cli::commands::output::OutputArgs;
/// Top-level GitHub command group
#[derive(Args, Debug)]
pub struct GitHubArgs {
#[command(subcommand)]
pub command: GitHubCommand,
/// Override GitHub API URL (e.g. Enterprise)
#[arg(global = true, long, default_value = "https://api.github.com/", value_hint = ValueHint::Url)]
pub github_api_url: Url,
}
#[derive(Subcommand, Debug)]
pub enum GitHubCommand {
/// Interact with GitHub repositories
#[command(subcommand)]
Repos(GitHubReposCommand),
}
#[derive(Subcommand, Debug)]
pub enum GitHubReposCommand {
/// List repositories for a user or organization
List(GitHubReposListArgs),
}
/// `kingfisher github repos`
#[derive(Args, Debug, Clone)]
pub struct GitHubReposListArgs {
#[command(flatten)]
pub repo_specifiers: GitHubRepoSpecifiers,
#[command(flatten)]
pub output_args: OutputArgs<GitHubOutputFormat>,
}
/// Options for selecting GitHub repos
#[derive(Args, Debug, Clone)]
pub struct GitHubRepoSpecifiers {
/// Repositories belonging to these users
#[arg(long, alias = "github-user")]
pub user: Vec<String>,
/// Repositories belonging to these organizations
#[arg(long, alias = "org", alias = "github-organization", alias = "github-org")]
pub organization: Vec<String>,
/// Repositories for all organizations (Enterprise only)
#[arg(
long,
alias = "all-orgs",
alias = "all-github-organizations",
alias = "all-github-orgs",
requires = "github_api_url"
)]
pub all_organizations: bool,
/// Filter by repository type
#[arg(long, default_value_t = GitHubRepoType::Source, alias = "github-repo-type")]
pub repo_type: GitHubRepoType,
}
impl GitHubRepoSpecifiers {
/// Check if no GitHub sources are specified
pub fn is_empty(&self) -> bool {
self.user.is_empty() && self.organization.is_empty() && !self.all_organizations
}
}
/// GitHub repository type filter
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitHubRepoType {
/// Both source and fork repositories
All,
/// Only source repositories (not forks)
Source,
/// Only fork repositories
#[value(alias = "forks")]
Fork,
}
impl From<GitHubRepoType> for crate::github::RepoType {
fn from(val: GitHubRepoType) -> Self {
match val {
GitHubRepoType::All => crate::github::RepoType::All,
GitHubRepoType::Source => crate::github::RepoType::Source,
GitHubRepoType::Fork => crate::github::RepoType::Fork,
}
}
}
/// Output formats for GitHub commands
#[derive(Copy, Clone, Debug, ValueEnum, Display)]
#[strum(serialize_all = "kebab-case")]
pub enum GitHubOutputFormat {
Pretty,
Json,
Jsonl,
Bson,
Sarif,
}
// -----------------------------------------------------------------------------
// Git Repository Cloning/History
// -----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitCloneMode {
/// Equivalent to `git clone --bare`
Bare,
/// Equivalent to `git clone --mirror`, often clones extra objects
Mirror,
}
/// Specifies how to handle a repository's Git history.
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitHistoryMode {
/// Scan all history
Full,
/// Ignore history entirely
None,
}

View file

@ -0,0 +1,91 @@
use clap::{Args, Subcommand, ValueEnum, ValueHint};
use strum_macros::Display;
use url::Url;
use crate::cli::commands::output::{GitHubOutputFormat, OutputArgs};
/// Top-level GitLab command group
#[derive(Args, Debug)]
pub struct GitLabArgs {
#[command(subcommand)]
pub command: GitLabCommand,
/// Override GitLab API URL (e.g. Enterprise)
#[arg(global = true, long, default_value = "https://gitlab.com/", value_hint = ValueHint::Url)]
pub gitlab_api_url: Url,
}
#[derive(Subcommand, Debug)]
pub enum GitLabCommand {
/// Interact with GitLab repositories
#[command(subcommand)]
Repos(GitLabReposCommand),
}
#[derive(Subcommand, Debug)]
pub enum GitLabReposCommand {
/// List repositories for a user or group
List(GitLabReposListArgs),
}
/// `kingfisher gitlab repos`
#[derive(Args, Debug, Clone)]
pub struct GitLabReposListArgs {
#[command(flatten)]
pub repo_specifiers: GitLabRepoSpecifiers,
#[command(flatten)]
pub output_args: OutputArgs<GitLabOutputFormat>,
}
/// Options for selecting GitLab repos
#[derive(Args, Debug, Clone)]
pub struct GitLabRepoSpecifiers {
/// Repositories belonging to these users
#[arg(long, alias = "gitlab-user")]
pub user: Vec<String>,
/// Repositories belonging to these groups
#[arg(long, alias = "gitlab-group")]
pub group: Vec<String>,
/// Repositories for all groups (Enterprise only)
#[arg(long, alias = "all-groups", alias = "all-gitlab-groups", requires = "gitlab_api_url")]
pub all_groups: bool,
/// Filter by repository type
#[arg(long, default_value_t = GitLabRepoType::All, alias = "gitlab-repo-type")]
pub repo_type: GitLabRepoType,
}
impl GitLabRepoSpecifiers {
/// Check if no GitLab sources are specified
pub fn is_empty(&self) -> bool {
self.user.is_empty() && self.group.is_empty() && !self.all_groups
}
}
/// GitLab repository type filter
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitLabRepoType {
/// All repositories the user/group has access to
All,
/// Only repositories owned by the user/group
Owner,
/// Only repositories where the user is a member
Member,
}
/// Output formats for GitLab commands - reusing GitHub's formats
pub type GitLabOutputFormat = GitHubOutputFormat;
impl From<GitLabRepoType> for crate::gitlab::RepoType {
fn from(val: GitLabRepoType) -> Self {
match val {
GitLabRepoType::All => crate::gitlab::RepoType::All,
GitLabRepoType::Owner => crate::gitlab::RepoType::Owner,
GitLabRepoType::Member => crate::gitlab::RepoType::Member,
}
}
}

140
src/cli/commands/inputs.rs Normal file
View file

@ -0,0 +1,140 @@
use std::path::PathBuf;
use clap::{Args, ValueHint};
use url::Url;
use crate::{
cli::commands::{
github::{GitCloneMode, GitHistoryMode, GitHubRepoType},
gitlab::GitLabRepoType,
},
git_url::GitUrl,
};
// -----------------------------------------------------------------------------
// Inputs
// -----------------------------------------------------------------------------
#[derive(Args, Debug, Clone)]
pub struct InputSpecifierArgs {
/// Scan this file, directory, or local Git repository
#[arg(
required_unless_present_any([
"github_user",
"github_organization",
"gitlab_user",
"gitlab_group",
"git_url",
"all_github_organizations",
"all_gitlab_groups"
]),
value_hint = ValueHint::AnyPath
)]
pub path_inputs: Vec<PathBuf>,
/// Clone and scan the Git repository at the given URL
#[arg(long, value_hint = ValueHint::Url)]
pub git_url: Vec<GitUrl>,
/// Scan repositories belonging to the specified GitHub user
#[arg(long)]
pub github_user: Vec<String>,
/// Scan repositories belonging to the specified GitHub organization
#[arg(long, alias = "github-org")]
pub github_organization: Vec<String>,
/// Scan repositories from all GitHub organizations (requires non-default --github-api-url)
#[arg(long, alias = "all-github-orgs", requires = "github_api_url")]
pub all_github_organizations: bool,
/// Use the specified URL for GitHub API access (e.g. for GitHub Enterprise)
#[arg(
long,
alias="api-url",
default_value = "https://api.github.com/",
value_hint = ValueHint::Url
)]
pub github_api_url: Url,
#[arg(long, default_value_t = GitHubRepoType::Source)]
pub github_repo_type: GitHubRepoType,
// GitLab Options
/// Scan repositories belonging to the specified GitLab user
#[arg(long)]
pub gitlab_user: Vec<String>,
/// Scan repositories belonging to the specified GitLab group
#[arg(long, alias = "gitlab-group")]
pub gitlab_group: Vec<String>,
/// Scan repositories from all GitLab groups (requires non-default --gitlab-api-url)
#[arg(long, alias = "all-gitlab-groups", requires = "gitlab_api_url")]
pub all_gitlab_groups: bool,
/// Use the specified URL for GitLab API access (e.g. for GitLab self-hosted)
#[arg(
long,
alias="gitlab-api-url",
default_value = "https://gitlab.com/",
value_hint = ValueHint::Url
)]
pub gitlab_api_url: Url,
#[arg(long, default_value_t = GitLabRepoType::Owner)]
pub gitlab_repo_type: GitLabRepoType,
/// Select how to clone Git repositories
#[arg(long, default_value_t=GitCloneMode::Bare, alias="git-clone-mode")]
pub git_clone: GitCloneMode,
/// Select whether to scan full Git history or not
#[arg(long, default_value_t=GitHistoryMode::Full)]
pub git_history: GitHistoryMode,
/// Include detailed Git commit context (author, date, commit hash) for findings.
/// Set to 'false' to disable.
#[arg(long, default_value_t = true, action = clap::ArgAction::Set, help_heading = "Git Options")]
pub commit_metadata: bool,
/// Enable or disable scanning nested git repositories
#[arg(long, default_value_t = true)]
pub scan_nested_repos: bool,
}
// -----------------------------------------------------------------------------
// Content Filtering
// -----------------------------------------------------------------------------
#[derive(Args, Debug, Clone)]
pub struct ContentFilteringArgs {
/// Ignore files larger than the given size in MB
#[arg(long("max-file-size"), default_value_t = 25.0)]
pub max_file_size_mb: f64,
/// Use custom path-based ignore rules from the given file(s)
#[arg(long, short, value_hint = ValueHint::FilePath)]
pub ignore: Vec<PathBuf>,
/// If true, do NOT extract archive files
#[arg(long("no-extract-archives"), default_value_t = false)]
pub no_extract_archives: bool,
/// Maximum allowed depth for extracting nested archives
#[arg(long("extraction-depth"), default_value_t = 2, value_parser = clap::value_parser!(u8).range(1..=25))]
pub extraction_depth: u8,
/// If true, do NOT scan binary files
#[arg(long("no-binary"), default_value_t = false)]
pub no_binary: bool,
}
impl ContentFilteringArgs {
/// Convert the maximum file size in MB to bytes
pub fn max_file_size_bytes(&self) -> Option<u64> {
if self.max_file_size_mb < 0.0 {
Some(25 * 1024 * 1024) // default 25 MB if negative
} else {
Some((self.max_file_size_mb * 1024.0 * 1024.0) as u64)
}
}
}

6
src/cli/commands/mod.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod github;
pub mod gitlab;
pub mod inputs;
pub mod output;
pub mod rules;
pub mod scan;

View file

@ -0,0 +1,77 @@
use std::path::PathBuf;
use clap::{Args, ValueEnum, ValueHint};
use strum::Display;
use crate::util::get_writer_for_file_or_stdout;
// -----------------------------------------------------------------------------
// Output Options
// -----------------------------------------------------------------------------
#[derive(Args, Debug, Clone)]
#[command(next_help_heading = "Output Options")]
pub struct OutputArgs<Format: ValueEnum + Send + Sync + 'static> {
/// Write output to the specified path (stdout if not given)
#[arg(long, short, value_hint = ValueHint::FilePath)]
pub output: Option<PathBuf>,
/// Output format (defaults to `pretty` if not specified)
#[arg(long, short, default_value = "pretty")]
pub format: Format,
}
impl<Format: ValueEnum + Send + Sync> OutputArgs<Format> {
/// Return a writer for the specified output destination
pub fn get_writer(&self) -> std::io::Result<Box<dyn std::io::Write>> {
get_writer_for_file_or_stdout(self.output.as_ref())
}
/// Check if an output path was specified
pub fn has_output(&self) -> bool {
self.output.is_some()
}
}
// -----------------------------------------------------------------------------
// Report Output Format
// -----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum ReportOutputFormat {
/// A human-friendly text-based format
Pretty,
/// Pretty-printed JSON
Json,
/// JSON Lines (one JSON object per line)
Jsonl,
/// BSON (binary JSON) format
Bson,
/// SARIF format (experimental)
Sarif,
}
// -----------------------------------------------------------------------------
// GitHub Output Format
// -----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum GitHubOutputFormat {
/// A human-friendly text-based format
Pretty,
/// Pretty-printed JSON
Json,
/// JSON Lines (one JSON object per line)
Jsonl,
/// BSON (binary JSON) format
Bson,
/// SARIF format (experimental)
Sarif,
}

75
src/cli/commands/rules.rs Normal file
View file

@ -0,0 +1,75 @@
use std::path::PathBuf;
use clap::{ArgAction, Args, Subcommand, ValueEnum, ValueHint};
use strum::Display;
use crate::cli::commands::output::OutputArgs;
// -----------------------------------------------------------------------------
// Rule Specifiers
// -----------------------------------------------------------------------------
#[derive(Args, Debug, Clone, Default)]
pub struct RuleSpecifierArgs {
/// Load additional rules from file(s) or directories
///
/// Directories are walked recursively for YAML files. This option
/// can be repeated.
#[arg(long, alias="rules", value_hint=ValueHint::AnyPath)]
pub rules_path: Vec<PathBuf>,
/// Enable the ruleset with the given ID (e.g. `all`, `default`, or custom)
///
/// Repeating this disables the default set unless `default` is explicitly included.
#[arg(long, default_values_t=["all".to_string()])]
pub rule: Vec<String>,
/// Load built-in rules
#[arg(long, default_value_t=true, action=ArgAction::Set)]
pub load_builtins: bool,
}
#[derive(Args, Debug)]
pub struct RulesArgs {
#[command(subcommand)]
pub command: RulesCommand,
}
#[derive(Subcommand, Debug)]
pub enum RulesCommand {
/// Check rules for problems
Check(RulesCheckArgs),
/// List available rules
List(RulesListArgs),
}
#[derive(Args, Debug)]
pub struct RulesCheckArgs {
/// Treat warnings as errors
#[arg(long, short = 'W')]
pub warnings_as_errors: bool,
#[command(flatten)]
pub rules: RuleSpecifierArgs,
}
#[derive(Args, Debug)]
pub struct RulesListArgs {
#[command(flatten)]
pub rules: RuleSpecifierArgs,
#[command(flatten)]
pub output_args: OutputArgs<RulesListOutputFormat>,
}
// -----------------------------------------------------------------------------
// Rules List Output Format
// -----------------------------------------------------------------------------
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum RulesListOutputFormat {
/// A human-friendly text-based format
Pretty,
/// Pretty-printed JSON
Json,
}

124
src/cli/commands/scan.rs Normal file
View file

@ -0,0 +1,124 @@
use clap::{Args, ValueEnum};
use strum::Display;
use tracing::debug;
use crate::{
cli::{
commands::{
inputs::{ContentFilteringArgs, InputSpecifierArgs},
output::{OutputArgs, ReportOutputFormat},
rules::RuleSpecifierArgs,
},
global::RAM_GB,
},
rules::rule::Confidence,
};
/// Determine the default number of parallel scan jobs.
///
/// * Target = `num_cpus * 2`.
/// * Cap by RAM at ≈ 1 GiB per job (so 16 GiB ⇒ max 16 jobs).
/// * Always ≥ 1.
/// * When `-v/--verbose` is passed, the computed value is logged at DEBUG.
fn default_scan_jobs() -> usize {
// How many logical CPUs do we see? (Falls back to 1 on error.)
let cpu_count = std::thread::available_parallelism().map(usize::from).unwrap_or(1);
// Desired parallelism is CPU * 2.
let desired = cpu_count * 2;
match *RAM_GB {
// If we know how much RAM we have, cap by a 1 GiB-per-job heuristic.
Some(ram_gb) => {
let max_by_ram = ram_gb.ceil() as usize; // 1 GiB per job
let jobs = desired.min(max_by_ram).max(1);
debug!(
"Using {jobs} parallel scan jobs \
(cpus = {cpu_count}, desired = {desired}, \
ram = {ram_gb:.1} GiB, cap_by_ram = {max_by_ram})"
);
jobs
}
// If RAM is unknown, just use the desired value.
None => {
debug!("Using {desired} parallel scan jobs (cpus = {cpu_count}, ram unknown)");
desired
}
}
}
/// `kingfisher scan` command and flags
#[derive(Args, Debug, Clone)]
pub struct ScanArgs {
/// Number of parallel scanning threads
#[arg(long = "jobs", short = 'j', default_value_t = default_scan_jobs())]
pub num_jobs: usize,
#[command(flatten)]
pub rules: RuleSpecifierArgs,
#[command(flatten)]
pub input_specifier_args: InputSpecifierArgs,
#[command(flatten)]
pub content_filtering_args: ContentFilteringArgs,
/// Minimum confidence level for reporting findings
#[arg(long, short = 'c', default_value = "medium")]
pub confidence: ConfidenceLevel,
/// Disable secret validation
#[arg(long, short = 'n', default_value_t = false)]
pub no_validate: bool,
/// Display only validated findings
#[arg(long, default_value_t = false)]
pub only_valid: bool,
/// Override the default minimum entropy threshold
#[arg(long, short = 'e')]
pub min_entropy: Option<f32>,
/// Show performance statistics for each rule
#[arg(long, default_value_t = false)]
pub rule_stats: bool,
/// Display every occurrence of a finding
#[arg(long, default_value_t = false)]
pub no_dedup: bool,
/// Redact findings values using a secure hash
#[arg(long, short = 'r', default_value_t = false)]
pub redact: bool,
/// Timeout for Git repository scanning in seconds
#[arg(long, default_value_t = 1800, value_name = "SECONDS")]
pub git_repo_timeout: u64,
#[command(flatten)]
pub output_args: OutputArgs<ReportOutputFormat>,
/// Bytes of context before and after each match
#[arg(long, default_value_t = 256, value_name = "BYTES")]
pub snippet_length: usize,
}
/// Confidence levels for findings
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
#[strum(serialize_all = "kebab-case")]
pub enum ConfidenceLevel {
Low,
Medium,
High,
}
impl From<ConfidenceLevel> for Confidence {
fn from(level: ConfidenceLevel) -> Self {
match level {
ConfidenceLevel::Low => Confidence::Low,
ConfidenceLevel::Medium => Confidence::Medium,
ConfidenceLevel::High => Confidence::High,
}
}
}

180
src/cli/global.rs Normal file
View file

@ -0,0 +1,180 @@
use std::io::IsTerminal;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use once_cell::sync::Lazy;
use strum::Display;
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
use tracing::Level;
use crate::cli::commands::{
github::GitHubArgs, gitlab::GitLabArgs, rules::RulesArgs, scan::ScanArgs,
};
#[deny(missing_docs)]
#[derive(Parser, Debug)]
#[command(
version = env!("CARGO_PKG_VERSION"),
after_help = "Made with \u{2764} by MongoDB",
)]
/// Kingfisher by MongoDB — Detect and validate secrets across files and full Git history
pub struct CommandLineArgs {
/// The command to execute
#[command(subcommand)]
pub command: Command,
/// Global arguments that apply to all subcommands
#[command(flatten)]
pub global_args: GlobalArgs,
}
impl CommandLineArgs {
/// Parse command-line arguments.
///
/// Automatically respects `NO_COLOR` and maps `--quiet` into disabling progress bars.
pub fn parse_args() -> Self {
// Use standard `Parser::parse` for simplicity
let mut args = CommandLineArgs::parse();
// Apply NO_COLOR environment variable
if std::env::var("NO_COLOR").is_ok() {
args.global_args.color = Mode::Never;
}
// If quiet is enabled, disable progress
if args.global_args.quiet {
args.global_args.progress = Mode::Never;
}
args
}
}
/// Top-level subcommands
#[derive(Subcommand, Debug)]
pub enum Command {
/// Scan content for secrets and sensitive information
Scan(ScanArgs),
/// Interact with the GitHub API
#[command(name = "github")]
GitHub(GitHubArgs),
/// Interact with the GitLab API
#[command(name = "gitlab")]
GitLab(GitLabArgs),
/// Manage rules
#[command(alias = "rule")]
Rules(RulesArgs),
}
pub static RAM_GB: Lazy<Option<f64>> = Lazy::new(|| {
if sysinfo::IS_SUPPORTED_SYSTEM {
let s = System::new_with_specifics(
RefreshKind::new().with_memory(MemoryRefreshKind::new().with_ram()),
);
Some(s.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0)
} else {
None
}
});
/// Advanced global options unlikely to be used in normal scenarios.
#[derive(Args, Debug, Clone)]
#[command(next_help_heading = "Advanced Global Options")]
pub struct AdvancedArgs {
/// Set the rlimit for the number of open files
#[arg(long, default_value_t = 16384, value_name = "LIMIT")]
pub rlimit_nofile: u64,
}
/// Top-level global CLI arguments
#[derive(Args, Debug, Clone)]
#[command(next_help_heading = "Global Options")]
pub struct GlobalArgs {
/// Enable verbose output (up to 3 times for more detail)
#[arg(global = true, long = "verbose", short = 'v', action = ArgAction::Count)]
pub verbose: u8,
/// Suppress non-error messages and disable progress bars
#[arg(global = true, long, short)]
pub quiet: bool,
/// Ignore TLS certificate validation
#[arg(global = true, long)]
pub ignore_certs: bool,
/// Update the Kingfisher binary to the latest release
#[arg(global = true, long = "self-update", default_value_t = false)]
pub self_update: bool,
/// Disable automatic update checks
#[arg(global = true, long = "no-update-check", default_value_t = false)]
pub no_update_check: bool,
#[command(flatten)]
pub advanced: AdvancedArgs,
// Internal fields (not CLI arguments)
#[clap(skip)]
pub color: Mode,
#[clap(skip)]
pub progress: Mode,
}
impl Default for GlobalArgs {
fn default() -> Self {
Self {
verbose: 0,
quiet: false,
ignore_certs: false,
self_update: false,
no_update_check: false,
advanced: AdvancedArgs { rlimit_nofile: 16384 },
color: Mode::Auto,
progress: Mode::Auto,
}
}
}
impl GlobalArgs {
pub fn use_color<T: IsTerminal>(&self, out: T) -> bool {
match self.color {
Mode::Never => false,
Mode::Always => true,
Mode::Auto => out.is_terminal(),
}
}
pub fn use_progress(&self) -> bool {
match self.progress {
Mode::Never => false,
Mode::Always => true,
Mode::Auto => std::io::stderr().is_terminal(),
}
}
pub fn log_level(&self) -> Level {
if self.quiet {
Level::INFO
} else {
match self.verbose {
0 => Level::INFO, // Default level if no `-v` is provided
1 => Level::DEBUG, // `-v`
2 => Level::TRACE, // `-vv`
_ => Level::TRACE, // `-vvv` or more
}
}
}
}
/// Mode for enabling or disabling features based on terminal capabilities
/// Generic mode with `auto/never/always`.
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
#[strum(serialize_all = "kebab-case")]
pub enum Mode {
#[default]
Auto,
Never,
Always,
}

5
src/cli/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod commands;
pub mod global;
// reexport the toplevel parser and subcommand enum so main.rs can see them:
pub use global::{CommandLineArgs, GlobalArgs};