Document every CLI argument
This commit is contained in:
parent
7d67da9dfe
commit
c20cc47914
2 changed files with 226 additions and 4 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
|
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
||||||
use clap_complete::{Shell, generate};
|
use clap_complete::{Shell, generate};
|
||||||
use geth_config::GethPaths;
|
use geth_config::GethPaths;
|
||||||
use geth_control::{ControlRequest, ControlResponse, SyncStreamStatus};
|
use geth_control::{ControlRequest, ControlResponse, SyncStreamStatus};
|
||||||
|
|
@ -727,6 +727,9 @@ pub enum NodeCommand {
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum NodeEnrollCommand {
|
pub enum NodeEnrollCommand {
|
||||||
/// Create a signed enrollment request on the new node
|
/// Create a signed enrollment request on the new node
|
||||||
|
#[command(
|
||||||
|
after_help = "Examples:\n geth node enroll request --node-name workstation --out workstation.enroll.json\n geth node enroll request --node-name ci-runner --capability resource:kv:builds=kv.read"
|
||||||
|
)]
|
||||||
Request {
|
Request {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
node_name: String,
|
node_name: String,
|
||||||
|
|
@ -988,6 +991,9 @@ pub enum AuthCommand {
|
||||||
/// Pull verified authorization operations from a trusted peer
|
/// Pull verified authorization operations from a trusted peer
|
||||||
Sync { node: String },
|
Sync { node: String },
|
||||||
/// Add an admin-signed resource capability grant
|
/// Add an admin-signed resource capability grant
|
||||||
|
#[command(
|
||||||
|
after_help = "Example:\n geth auth grant <node-id> resource:kv:preferences kv.read --signing-key ~/.ssh/id_ed25519"
|
||||||
|
)]
|
||||||
Grant {
|
Grant {
|
||||||
subject: String,
|
subject: String,
|
||||||
resource: String,
|
resource: String,
|
||||||
|
|
@ -1138,6 +1144,9 @@ pub enum CasRootCommand {
|
||||||
bearer_secret: Option<String>,
|
bearer_secret: Option<String>,
|
||||||
},
|
},
|
||||||
/// Materialize a tree without overwriting local edits
|
/// Materialize a tree without overwriting local edits
|
||||||
|
#[command(
|
||||||
|
after_help = "Examples:\n geth cas root apply photos --to ./restored-photos --dry-run\n geth cas root apply remote-<peer-id>-photos --to ./restored-photos"
|
||||||
|
)]
|
||||||
Apply {
|
Apply {
|
||||||
source: String,
|
source: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -1182,6 +1191,9 @@ pub enum KvCommand {
|
||||||
/// Create a named local key-value store
|
/// Create a named local key-value store
|
||||||
Create { name: String },
|
Create { name: String },
|
||||||
/// Set a local key, optionally checking a non-owner subject
|
/// Set a local key, optionally checking a non-owner subject
|
||||||
|
#[command(
|
||||||
|
after_help = "Examples:\n geth kv set preferences theme dark\n geth kv set preferences theme dark --subject <node-id>"
|
||||||
|
)]
|
||||||
Set {
|
Set {
|
||||||
name: String,
|
name: String,
|
||||||
key: String,
|
key: String,
|
||||||
|
|
@ -1459,7 +1471,8 @@ pub enum SshRevocationCommand {
|
||||||
pub struct EmptyArgs {}
|
pub struct EmptyArgs {}
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let matches = documented_cli_command().get_matches();
|
||||||
|
let cli = Cli::from_arg_matches(&matches).context("parse geth command")?;
|
||||||
let json = cli.json || cli.jsonl;
|
let json = cli.json || cli.jsonl;
|
||||||
match run_inner(cli).await {
|
match run_inner(cli).await {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
|
|
@ -1471,6 +1484,173 @@ pub async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn documented_cli_command() -> clap::Command {
|
||||||
|
document_missing_arguments(Cli::command(), "geth")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_missing_arguments(mut command: clap::Command, path: &str) -> clap::Command {
|
||||||
|
let command_path = path.to_owned();
|
||||||
|
command = command.mut_args(|argument| {
|
||||||
|
if argument.get_help().is_some() {
|
||||||
|
argument
|
||||||
|
} else if let Some(help) = argument_help(&command_path, argument.get_id().as_str()) {
|
||||||
|
argument.help(help)
|
||||||
|
} else {
|
||||||
|
argument
|
||||||
|
}
|
||||||
|
});
|
||||||
|
command.mut_subcommands(|subcommand| {
|
||||||
|
let child_path = format!("{path} {}", subcommand.get_name());
|
||||||
|
document_missing_arguments(subcommand, &child_path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn argument_help(path: &str, id: &str) -> Option<&'static str> {
|
||||||
|
let contextual = match (path, id) {
|
||||||
|
("geth resource create", "kind") => {
|
||||||
|
Some("Resource kind, such as cas, kv, document, db, pipe, or pubsub")
|
||||||
|
}
|
||||||
|
("geth overlay interface-plan", "platform") => {
|
||||||
|
Some("Target platform: linux, macos, or windows; defaults to this host")
|
||||||
|
}
|
||||||
|
("geth ssh cert request", "kind") => Some("Certificate kind: user or host"),
|
||||||
|
("geth ssh revocation add", "kind") => {
|
||||||
|
Some("Revocation kind: public-key, certificate, serial, or key-id")
|
||||||
|
}
|
||||||
|
("geth cas conflict record", "kind") => {
|
||||||
|
Some("Conflict kind, such as concurrent-edit, delete-edit, or divergent-rename")
|
||||||
|
}
|
||||||
|
("geth pipe connect" | "geth pipe send", "target") => Some("Registered pipe listener name"),
|
||||||
|
("geth pipe forward-tcp", "target") => {
|
||||||
|
Some("Remote loopback TCP target, for example 127.0.0.1:5432")
|
||||||
|
}
|
||||||
|
("geth pipe forward-unix", "target") => Some("Absolute Unix socket path on the peer"),
|
||||||
|
("geth ssh revocation add", "target") => {
|
||||||
|
Some("Key, certificate, serial, or key-id selected by KIND")
|
||||||
|
}
|
||||||
|
("geth ssh admin-shell", "command") => Some("Restricted command: help, status, or node-id"),
|
||||||
|
("geth node enroll list", "status") => {
|
||||||
|
Some("Optional request status filter: pending, approved, or rejected")
|
||||||
|
}
|
||||||
|
("geth ssh revocation export" | "geth ssh revocation import", "format") => {
|
||||||
|
Some("Format: jsonl, openssh, or krl where supported")
|
||||||
|
}
|
||||||
|
("geth cas root apply", "source") => {
|
||||||
|
Some("Local or peer-qualified file-root name to materialize")
|
||||||
|
}
|
||||||
|
("geth cas conflict resolve", "resolution") => {
|
||||||
|
Some("Resolution: keep-local, accept-remote, keep-both, or manual")
|
||||||
|
}
|
||||||
|
("geth node enroll request", "capabilities") => {
|
||||||
|
Some("Requested RESOURCE=CAPABILITY pair; repeat to request more than one")
|
||||||
|
}
|
||||||
|
("geth kv create" | "geth kv set" | "geth kv get" | "geth kv sync", "name") => {
|
||||||
|
Some("Name of the KV store")
|
||||||
|
}
|
||||||
|
("geth kv set" | "geth kv get", "key") => Some("Key within the named KV store"),
|
||||||
|
("geth db add" | "geth db status" | "geth db changes" | "geth db sync", "name") => {
|
||||||
|
Some("Name of the database resource")
|
||||||
|
}
|
||||||
|
(
|
||||||
|
"geth document create"
|
||||||
|
| "geth document set"
|
||||||
|
| "geth document get"
|
||||||
|
| "geth document sync",
|
||||||
|
"name",
|
||||||
|
) => Some("Name of the document resource"),
|
||||||
|
("geth cas root add" | "geth cas root scan" | "geth cas root sync", "name") => {
|
||||||
|
Some("Name of the file root")
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
contextual.or(match id {
|
||||||
|
"topic" => Some("Embedded guide topic; omit it to list available topics"),
|
||||||
|
"shell" => Some("Shell whose completion script should be generated"),
|
||||||
|
"node" | "owner_node" => Some("Trusted node id or friendly node name"),
|
||||||
|
"timeout_ms" => Some("Maximum time to wait, in milliseconds"),
|
||||||
|
"interval_ms" => Some("Delay between readiness checks, in milliseconds"),
|
||||||
|
"out" => Some("Output file or directory; command output is used when omitted if supported"),
|
||||||
|
"backup_dir" => Some("Backup directory containing manifest.json and the home payload"),
|
||||||
|
"target_home" => Some("New empty directory that will receive the restored home"),
|
||||||
|
"node_name" => Some("Human-friendly name for the node"),
|
||||||
|
"capabilities" | "capability" => {
|
||||||
|
Some("Resource capability name; repeat the flag where supported")
|
||||||
|
}
|
||||||
|
"reason" => Some("Optional operator-readable reason recorded with the operation"),
|
||||||
|
"request_id" => Some("Enrollment or certificate request identifier"),
|
||||||
|
"path" => Some("Local input file or directory path"),
|
||||||
|
"signing_key" => {
|
||||||
|
Some("Private OpenSSH key, public agent key, or FIDO/YubiKey key stub used to sign")
|
||||||
|
}
|
||||||
|
"admin_key" => Some("Matching OpenSSH admin public key; inferred where possible"),
|
||||||
|
"name" => Some("Name of the command-specific local object"),
|
||||||
|
"resource" => Some("Canonical resource id, for example resource:kv:preferences"),
|
||||||
|
"grant_id" => Some("Stable grant identifier; generated when omitted where supported"),
|
||||||
|
"endpoint" => Some("Iroh EndpointID to bind to or revoke from the node"),
|
||||||
|
"kind" => Some("Command-specific object kind"),
|
||||||
|
"cidr" => Some("Overlay IPv4 CIDR, for example 172.22.0.0/24"),
|
||||||
|
"secret" => Some("Private resource or bearer token; it is never stored in command logs"),
|
||||||
|
"platform" => Some("Target operating-system platform"),
|
||||||
|
"bearer_secret" => Some("Optional resource-scoped bearer token for remote authorization"),
|
||||||
|
"mtu" => Some("Overlay interface MTU in bytes"),
|
||||||
|
"packet_base64" => Some("Complete IPv4 packet encoded as base64"),
|
||||||
|
"peek" => Some("Read current data without draining it"),
|
||||||
|
"principal" | "principals" => {
|
||||||
|
Some("OpenSSH signer or certificate principal; repeat where supported")
|
||||||
|
}
|
||||||
|
"valid_after_ms" => Some("Earliest validity time as Unix milliseconds"),
|
||||||
|
"valid_before_ms" => Some("Latest validity time as Unix milliseconds"),
|
||||||
|
"input" => Some("Input file path"),
|
||||||
|
"namespace" => Some("SSH signature namespace; defaults to the relevant geth namespace"),
|
||||||
|
"signature" => Some("OpenSSH signature file path"),
|
||||||
|
"allowed_signers" => Some("OpenSSH allowed_signers file used for verification"),
|
||||||
|
"base_url" => Some("Publication base URL recorded in signed discovery metadata"),
|
||||||
|
"snapshots" => Some("Named snapshot mapping NAME=PATH; repeatable"),
|
||||||
|
"checkpoint" => Some("Signed publication checkpoint file"),
|
||||||
|
"sigchain" => Some("Canonical keychain signature-chain JSONL file"),
|
||||||
|
"url" => Some("HTTPS URL to fetch"),
|
||||||
|
"import" => Some("Import the verified result into local state"),
|
||||||
|
"op_id" => Some("Canonical keychain operation identifier"),
|
||||||
|
"subject" => Some("Principal evaluated or authorized instead of the local owner"),
|
||||||
|
"expires_at_ms" => Some("Optional bearer expiration as Unix milliseconds"),
|
||||||
|
"token" => Some("Private bearer token returned when access was created"),
|
||||||
|
"nonce" => Some("Challenge nonce returned by the challenge command"),
|
||||||
|
"response" => Some("Bearer possession proof response"),
|
||||||
|
"bearer_id" => Some("Public bearer-access identifier, not the private token"),
|
||||||
|
"hash" => Some("BLAKE3 CAS blob hash"),
|
||||||
|
"dry_run" => Some("Preview changes without mutating local state or files"),
|
||||||
|
"source" => Some("Command-specific source object"),
|
||||||
|
"to" => Some("Destination directory"),
|
||||||
|
"root" => Some("File-root name; omit the filter to include all roots"),
|
||||||
|
"detail" => Some("Operator-readable conflict detail"),
|
||||||
|
"base_tree" => Some("Optional common-ancestor CAS tree hash"),
|
||||||
|
"local_tree" => Some("Optional local CAS tree hash"),
|
||||||
|
"remote_tree" => Some("Optional remote CAS tree hash"),
|
||||||
|
"conflict_id" => Some("Durable file-conflict identifier"),
|
||||||
|
"resolution" => Some("Operator-selected conflict resolution"),
|
||||||
|
"note" => Some("Optional operator note recorded with the action"),
|
||||||
|
"value" => Some("UTF-8 value to store"),
|
||||||
|
"key" => Some("Key name, signer id, or fingerprint selected by the command"),
|
||||||
|
"message" => Some("UTF-8 message payload"),
|
||||||
|
"target" => Some("Command-specific destination or target"),
|
||||||
|
"listen" => Some("Local loopback address or absolute Unix socket path to listen on"),
|
||||||
|
"after_db_version" => Some("Return changes strictly after this cr-sqlite db_version"),
|
||||||
|
"limit" => Some("Maximum number of change rows to read or synchronize"),
|
||||||
|
"state_json" => Some("Complete JSON object or value used as the new document state"),
|
||||||
|
"command" => Some("Restricted command name"),
|
||||||
|
"public_key" => Some("OpenSSH public key to certify"),
|
||||||
|
"valid_for" => Some("OpenSSH validity interval, for example 8h or 7d"),
|
||||||
|
"renewal_of" => Some("Certificate id this request renews"),
|
||||||
|
"serial" => Some("Explicit OpenSSH certificate serial number"),
|
||||||
|
"sign" => Some("Run ssh-keygen now and import the generated certificate"),
|
||||||
|
"cert" => Some("Signed OpenSSH certificate file"),
|
||||||
|
"format" => Some("Input or output format selected by the command"),
|
||||||
|
"ca_public" => Some("OpenSSH CA public key used for KRL generation"),
|
||||||
|
"ca_key" => Some("OpenSSH CA private key or hardware-key stub used to sign"),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_inner(cli: Cli) -> Result<()> {
|
async fn run_inner(cli: Cli) -> Result<()> {
|
||||||
if let Command::Completions { shell } = &cli.command {
|
if let Command::Completions { shell } = &cli.command {
|
||||||
print_completions(*shell);
|
print_completions(*shell);
|
||||||
|
|
@ -1771,7 +1951,7 @@ fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_completions(shell: Shell) {
|
fn print_completions(shell: Shell) {
|
||||||
let mut command = Cli::command();
|
let mut command = documented_cli_command();
|
||||||
generate(shell, &mut command, "geth", &mut stdout());
|
generate(shell, &mut command, "geth", &mut stdout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4619,7 +4799,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn help_describes_common_workflows_and_command_families() {
|
fn help_describes_common_workflows_and_command_families() {
|
||||||
let help = Cli::command().render_long_help().to_string();
|
let help = documented_cli_command().render_long_help().to_string();
|
||||||
assert!(help.contains("geth daemon install"));
|
assert!(help.contains("geth daemon install"));
|
||||||
assert!(help.contains("geth daemon run --ephemeral"));
|
assert!(help.contains("geth daemon run --ephemeral"));
|
||||||
assert!(help.contains("Show daemon, storage, Iroh, and backend health"));
|
assert!(help.contains("Show daemon, storage, Iroh, and backend health"));
|
||||||
|
|
@ -4627,6 +4807,36 @@ mod tests {
|
||||||
assert!(help.contains("--home <DIR>"));
|
assert!(help.contains("--home <DIR>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_cli_argument_has_operator_facing_help() {
|
||||||
|
fn check(command: clap::Command, path: String, missing: &mut Vec<String>) {
|
||||||
|
for argument in command.get_arguments() {
|
||||||
|
let id = argument.get_id().as_str();
|
||||||
|
if matches!(id, "help" | "version") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if argument.get_help().is_none() {
|
||||||
|
missing.push(format!("{path}: {id}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subcommand in command.get_subcommands() {
|
||||||
|
check(
|
||||||
|
subcommand.clone(),
|
||||||
|
format!("{path} {}", subcommand.get_name()),
|
||||||
|
missing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
check(documented_cli_command(), "geth".to_owned(), &mut missing);
|
||||||
|
assert!(
|
||||||
|
missing.is_empty(),
|
||||||
|
"missing argument help:\n{}",
|
||||||
|
missing.join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn common_daemon_lifecycle_commands_parse_directly() {
|
fn common_daemon_lifecycle_commands_parse_directly() {
|
||||||
for command in ["install", "start", "stop", "status", "uninstall"] {
|
for command in ["install", "start", "stop", "status", "uninstall"] {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,18 @@ For deployment-readiness work that cuts across feature areas, see
|
||||||
- `[x]` Tests cover lifecycle parsing, help discoverability, and explicit
|
- `[x]` Tests cover lifecycle parsing, help discoverability, and explicit
|
||||||
home selection without mutating a real user service manager.
|
home selection without mutating a real user service manager.
|
||||||
|
|
||||||
|
- `[x]` Make leaf-command arguments self-explanatory.
|
||||||
|
Acceptance criteria:
|
||||||
|
- `[x]` Every positional argument and option has operator-facing help text,
|
||||||
|
including fields generated across deeply nested command families.
|
||||||
|
- `[x]` Ambiguous fields such as KV keys, resource names, conflict kinds, and
|
||||||
|
forwarding targets use command-specific wording where it affects correct
|
||||||
|
use.
|
||||||
|
- `[x]` Enrollment requests, authorization grants, CAS root application, and
|
||||||
|
KV writes include copy-paste examples in their leaf help.
|
||||||
|
- `[x]` A recursive CLI test fails when a future argument is added without a
|
||||||
|
description.
|
||||||
|
|
||||||
- `[x]` Document task-oriented user stories.
|
- `[x]` Document task-oriented user stories.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[x]` Workflows cover disposable evaluation, persistent background use,
|
- `[x]` Workflows cover disposable evaluation, persistent background use,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue