Files
porthole/src/cli.rs
Overlord ab0fd69503 Show [name] for optional positionals, allow filtering import by name too
The custom --help renderer showed every positional as <name> regardless
of whether it was actually required, which was misleading for open and
transfer (both take an optional profile name). positional_args() now
checks Arg::is_required_set() and renders [name] for an optional one.

transfer's name positional previously only worked with --export and was
rejected outright when combined with --import. It now filters --import
the same way: 'porthole transfer <name> -i file.toml' imports just that
one profile out of the file instead of everything in it, erroring with
a new TransferProfileNotFound if the file doesn't contain it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 00:54:27 +02:00

202 lines
5.2 KiB
Rust

use clap::{Args, Parser, Subcommand};
use clap_complete::Shell;
/// Create and manage named SSH port forwards.
#[derive(Parser)]
#[command(name = "porthole", version, about)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Save a new forward profile.
#[command(visible_alias = "create", visible_alias = "new")]
Add(AddArgs),
/// Start a saved forward as a supervised background process.
#[command(visible_alias = "start")]
Open(OpenArgs),
/// Stop a running forward.
#[command(visible_alias = "stop")]
Close(CloseArgs),
/// Update a saved profile.
Edit(EditArgs),
/// Show detailed status for one forward.
Status(StatusArgs),
/// List all saved profiles with live status.
#[command(visible_alias = "ls")]
List(ListArgs),
/// Delete a saved profile.
#[command(visible_alias = "rm", visible_alias = "delete")]
Remove(RemoveArgs),
/// Close and delete every forward, tracked or not.
#[command(visible_alias = "reset")]
Wipe(WipeArgs),
/// Export saved profiles to a file, or import them from one.
Transfer(TransferArgs),
/// Generate a shell completion script.
Completions { shell: Shell },
/// Internal: runs the supervisor loop for one profile. Not for direct
/// use - `open` spawns this itself (spec §3).
#[command(hide = true, name = "__supervise")]
Supervise { name: String },
}
/// Shared mapping/connection flags for `add` and `edit` - kept as one
/// struct (`#[command(flatten)]`ed into both) so the two can never drift.
#[derive(Args, Default)]
pub struct MappingArgs {
/// Local forward (current machine -> remote machine).
#[arg(short, long, value_name = "[BIND:]PORT:HOST:PORT")]
pub local: Option<String>,
/// Remote forward (remote machine -> current machine).
#[arg(short, long, value_name = "[BIND:]PORT:HOST:PORT")]
pub remote: Option<String>,
/// Dynamic forward (SOCKS proxy).
#[arg(short, long, value_name = "[BIND:]PORT")]
pub dynamic: Option<String>,
/// Jump-host chain, ending at the connection target.
#[arg(long, value_name = "[USER@]HOST[:PORT]", value_delimiter = ',')]
pub via: Vec<String>,
/// Default user for the target and any hop without one.
#[arg(short, long, value_name = "USER")]
pub user: Option<String>,
/// Identity file override.
#[arg(short, long, value_name = "PATH")]
pub identity: Option<String>,
/// SSH port of the final target.
#[arg(short, long, value_name = "PORT")]
pub port: Option<u16>,
/// Auto-reconnect on connection drop.
#[arg(long, num_args = 0..=1, default_missing_value = "true", value_name = "BOOL")]
pub reconnect: Option<bool>,
/// Base delay between reconnection attempts, in seconds.
#[arg(long = "retry-interval", value_name = "SECONDS")]
pub retry_interval: Option<u32>,
/// Cap on the doubling reconnection delay, in seconds.
#[arg(long = "backoff-max", value_name = "SECONDS")]
pub backoff_max: Option<u32>,
/// SSH ServerAliveInterval, in seconds.
#[arg(long, value_name = "SECONDS")]
pub keepalive: Option<u32>,
}
#[derive(Args)]
pub struct AddArgs {
/// Name of the new profile.
pub name: String,
#[command(flatten)]
pub mapping: MappingArgs,
}
#[derive(Args)]
pub struct EditArgs {
/// Name of the profile to edit.
pub name: String,
#[command(flatten)]
pub mapping: MappingArgs,
}
#[derive(Args)]
pub struct OpenArgs {
/// Name of the profile to open (ignored with --all).
pub name: Option<String>,
/// Run attached in the current shell instead of detaching.
#[arg(short, long)]
pub foreground: bool,
/// Open without auto-reconnect, regardless of the profile setting.
#[arg(long)]
pub once: bool,
/// Open every profile with reconnect enabled that isn't already open.
#[arg(long)]
pub all: bool,
}
#[derive(Args)]
pub struct CloseArgs {
/// Name of the profile to close.
pub name: String,
/// Send SIGKILL immediately instead of SIGTERM with a graceful wait.
#[arg(long)]
pub force: bool,
}
#[derive(Args)]
pub struct StatusArgs {
/// Name of the profile to inspect.
pub name: String,
/// Machine-readable output.
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct ListArgs {
/// Show only currently-open forwards.
#[arg(long)]
pub running: bool,
/// Machine-readable output.
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct RemoveArgs {
/// Name of the profile to delete.
pub name: String,
/// Delete the profile but leave an active instance running untracked.
#[arg(long = "keep-running")]
pub keep_running: bool,
}
#[derive(Args)]
pub struct WipeArgs {
/// Skip the confirmation prompt.
#[arg(short, long)]
pub yes: bool,
}
#[derive(Args)]
pub struct TransferArgs {
/// Restrict export/import to just this profile; every profile in scope if omitted.
pub name: Option<String>,
/// Export saved profiles to a file.
#[arg(short, long, value_name = "PATH.toml")]
pub export: Option<String>,
/// Import profiles from a file.
#[arg(short, long, value_name = "PATH.toml")]
pub import: Option<String>,
}