gitaway 0.1.0

Reach your self-hosted git server from anywhere, with a tunnel only gitaway uses
Documentation
//! clap definitions for gitaway's own commands.
//!
//! The structs here only describe the command line: names, flags and help
//! text. The code that acts on them is in the `*_cmd` modules. Values are
//! kept as strings and validated by the command, so every error comes from
//! the same validation the profile file uses.

use clap::{Args, Parser, Subcommand};

/// gitaway's own commands.
#[derive(Debug, Parser)]
#[command(
    name = "gitaway",
    version,
    about = "Reach your self-hosted git server from anywhere"
)]
pub struct Cli {
    /// The command to run.
    #[command(subcommand)]
    pub command: Command,
}

/// A gitaway command.
#[derive(Debug, Subcommand)]
pub enum Command {
    /// Add, list, show, change and remove profiles.
    #[command(subcommand)]
    Profile(ProfileCommand),
    /// Pin the current repository to a profile.
    Use(UseArgs),
    /// Print what the WireGuard server needs to know about this device.
    ///
    /// Prints a `[Peer]` block for the server's configuration: this device's
    /// public key and tunnel address. The private key never leaves this
    /// device.
    Peer(PeerArgs),
    /// Manage this device's WireGuard keys.
    #[command(subcommand)]
    Key(KeyCommand),
    /// Set up a profile from the VPN config your server gave this device.
    ///
    /// Asks for two things: the VPN config file (a WireGuard `.conf`, as
    /// the server or its app hands out) and a clone URL from your git
    /// server, as you use it at home. It then finds the git server, through
    /// the tunnel or, if you're home, directly, and saves its host key so
    /// gitaway can tell when you're home. Nothing is saved unless that
    /// works, or you confirm you're home.
    Setup(SetupArgs),
    /// Test a profile's tunnel and pin its home server's host key.
    ///
    /// At home, with a key pinned, it checks the key directly and stops:
    /// the tunnel can't usually be reached from inside the home network.
    /// Elsewhere, it opens the tunnel, reaches the `[home]` SSH server
    /// through it, and has that server prove its host key. The tunnel is
    /// authenticated by WireGuard, so this is safe from any network. With no
    /// key pinned yet, it offers to pin this one; if the pinned key differs,
    /// it says so and offers to replace it. Finally it runs the home check.
    Check(CheckArgs),
}

/// `gitaway setup`
#[derive(Debug, Args)]
pub struct SetupArgs {
    /// The new profile's name: lower-case letters, digits, `-` and `_`.
    /// Defaults to `home`.
    pub name: Option<String>,
    /// The VPN config file your server gave this device, or `-` to read it
    /// from stdin.
    #[arg(long, value_name = "FILE")]
    pub config: Option<String>,
    /// A clone URL from your git server, as you use it at home.
    #[arg(long)]
    pub url: Option<String>,
    /// The git server's SSH port, for an http(s) URL. gitaway connects to it
    /// to recognise home.
    #[arg(long, value_name = "PORT")]
    pub ssh_port: Option<String>,
    /// The git server's address inside the tunnel, if setup can't work it
    /// out from the config.
    #[arg(long, value_name = "ADDRESS")]
    pub inside: Option<String>,
    /// If the git server answers on this network, take that you're home and
    /// save its host key without asking.
    #[arg(long, short)]
    pub yes: bool,
}

/// `gitaway check`
#[derive(Debug, Args)]
pub struct CheckArgs {
    /// The profile. Defaults to the one this repository or your settings
    /// choose.
    pub profile: Option<String>,
    /// Pin or replace the home server's host key without asking.
    #[arg(long, short)]
    pub yes: bool,
}

/// `gitaway peer`
#[derive(Debug, Args)]
pub struct PeerArgs {
    /// The profile. Defaults to the one this repository or your settings
    /// choose.
    pub profile: Option<String>,
}

/// `gitaway key …`
#[derive(Debug, Subcommand)]
pub enum KeyCommand {
    /// Replace a profile's device key with a new one.
    ///
    /// The server must then be given the new public key, which this prints:
    /// until it is, the tunnel for this profile won't connect.
    Rotate {
        /// The profile. Defaults to the one this repository or your settings
        /// choose.
        profile: Option<String>,
        /// Don't ask for confirmation.
        #[arg(long, short)]
        yes: bool,
    },
    /// Use an existing private key as a profile's device key.
    ///
    /// For when the server or a WireGuard app already made this device's
    /// key pair. The key is read from stdin, never from the command line:
    /// pipe in the key or a whole WireGuard config with its `PrivateKey`
    /// line (`gitaway key import home < client.conf`), or, in a terminal,
    /// paste it when asked.
    Import {
        /// The profile. Defaults to the one this repository or your settings
        /// choose.
        profile: Option<String>,
        /// Replace an existing device key without asking.
        #[arg(long, short)]
        yes: bool,
    },
}

/// `gitaway profile …`
#[derive(Debug, Subcommand)]
pub enum ProfileCommand {
    /// Create a profile. Asks for anything not given as a flag.
    Add(AddArgs),
    /// List profiles. The default is marked with `*`.
    List,
    /// Show a profile's settings.
    Show {
        /// The profile to show.
        name: String,
    },
    /// Delete a profile and its device key.
    Remove {
        /// The profile to delete.
        name: String,
        /// Don't ask for confirmation.
        #[arg(long, short)]
        yes: bool,
    },
    /// Make a profile the default, used when nothing more specific applies.
    Default {
        /// The profile to make the default.
        name: String,
    },
    /// Change one setting, keeping the file's comments and layout.
    ///
    /// Keys are dotted paths, such as `home.ssh` or
    /// `transport.wireguard.mtu`. Quote parts that contain dots:
    /// `'hosts."192.0.2.10"'`. The change is checked before it's saved.
    Set {
        /// The profile to change.
        name: String,
        /// The setting, as a dotted path.
        key: String,
        /// The new value. Numbers and `true`/`false` are read as such;
        /// anything else is a string.
        value: String,
    },
}

/// `gitaway profile add`
#[derive(Debug, Args)]
pub struct AddArgs {
    /// The profile's name: lower-case letters, digits, `-` and `_`.
    pub name: Option<String>,
    /// The WireGuard server's public address, as host:port.
    #[arg(long)]
    pub endpoint: Option<String>,
    /// The WireGuard server's public key. In a client config it's the
    /// `PublicKey` under `[Peer]`, not the client's own public key.
    #[arg(long)]
    pub server_public_key: Option<String>,
    /// This device's tunnel address, such as 10.0.0.2/32, if already known.
    #[arg(long)]
    pub address: Option<String>,
    /// The SSH server used to check you're home, as host:port.
    #[arg(long)]
    pub home_ssh: Option<String>,
    /// That SSH server's public host key (one line of `ssh-keyscan` output,
    /// without the host name).
    #[arg(long)]
    pub host_key: Option<String>,
    /// A server this profile handles, as LAN=TUNNEL (for example
    /// 192.0.2.10=10.0.0.1). Repeat for several.
    #[arg(long = "host", value_name = "LAN=TUNNEL")]
    pub hosts: Vec<String>,
    /// Make this the default profile.
    #[arg(long)]
    pub default: bool,
}

/// `gitaway use`
#[derive(Debug, Args)]
pub struct UseArgs {
    /// The profile to pin this repository to.
    pub profile: String,
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::CommandFactory;

    #[test]
    fn definitions_are_valid() {
        Cli::command().debug_assert();
    }
}