shep 0.18.0

The shep binary: a process manager that keeps a flock of long-running processes alive on macOS, Linux and Windows, with logs, watch and cron restarts, and webhook alerts
Documentation
//! Arguments for the dog verbs: `dogs`, `enable`, `disable`, `adopt`,
//! the hidden `rehome`, and the hidden `dog` re-exec target.
//!
//! `enable` and `disable` also take a sheep's name, so each carries a
//! sheep-and-dog `name` of its own. The rest name a dog and never select
//! one, so none of them takes the selector grammar the flock verbs share.
//! Two of the six get by on the one name field [`DogArgs`] carries; the docs
//! on the others say what they add and why it was not worth widening the
//! shared struct.

use std::path::PathBuf;

/// `shep dogs`, the index of dogs you could adopt, and the two verbs that
/// change one permission of a dog.
///
/// The listing's flag and filter and the subcommands exclude each other:
/// `shep dogs grant` is a verb, so a dog named `grant` is reached with
/// `shep dogs -- grant`.
#[derive(Debug, clap::Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct DogsArgs {
    /// List the dogs published in the community index instead of the ones
    /// this shepherd is running. Needs no shepherd.
    #[arg(long)]
    pub available: bool,
    /// Narrow the listing to entries whose name, package or description
    /// contains this text, case-insensitively.
    #[arg(value_name = "FILTER")]
    pub filter: Option<String>,
    /// Change one permission instead of listing.
    #[command(subcommand)]
    pub command: Option<DogsCommand>,
}

/// `shep dogs`'s subcommands.
#[derive(Debug, clap::Subcommand)]
pub enum DogsCommand {
    /// Grant a dog one permission it declared.
    ///
    /// Writes `[daemon.dog_grants.<dog>]` in `shep.toml` and tells a
    /// running shepherd, which judges the dog's next request by the new
    /// grants with no restart. Only a permission the dog's manifest declares
    /// can be granted; `shep adopt` and the first `shep enable` ask about
    /// the whole list.
    ///
    /// Grants keep a dog's requests in bounds and do not confine it: it
    /// runs as you and can do anything you can.
    Grant(GrantArgs),
    /// Revoke one permission from a dog.
    ///
    /// Writes `[daemon.dog_grants.<dog>]` in `shep.toml` and tells a
    /// running shepherd, which refuses the dog's next request that needs
    /// it with no restart.
    Revoke(GrantArgs),
}

/// Arguments to `shep dogs grant` and `shep dogs revoke`.
#[derive(Debug, clap::Args)]
pub struct GrantArgs {
    /// The dog's name, as `shep adopt` registered it
    pub name: String,
    /// The permission, as the dog's manifest spells it: `flock.read`,
    /// `events:process.`, `secrets.read:staging`
    pub permission: String,
}

/// Arguments to the hidden `shep rehome`, and to the hidden `shep dog`
/// re-exec target.
///
/// One struct for both, matching [`StartupArgs`](crate::cli::StartupArgs)'s own precedent: a
/// dog is named, never selected — `SelectorArgs`' grammar (`all`, `/regex/`,
/// `fold:<name>`) answers "which of the flock", and a dog is not the flock.
/// `shep enable` carries a second, hidden field ([`EnableArgs`]) and both it
/// and `shep disable` ([`DisableArgs`]) also take a sheep's name, so each
/// gets a struct of its own rather than widening this one.
#[derive(Debug, clap::Args)]
pub struct DogArgs {
    /// The dog's name, the `[<name>]` config key in `dogs.toml`
    pub name: String,
}

/// Arguments to `shep enable`.
///
/// [`DogArgs`] plus one hidden field: `--exec` is pm2's own spelling of
/// `shep adopt`, kept as a working alias so muscle memory carries over —
/// `#[arg(hide = true)]`, not `#[command(alias = ..)]`, because the alias
/// is on an argument, not the subcommand itself. `shep enable --exec <path>
/// <name>` parses here and is routed to [`super::commands::dogs::adopt`](crate::commands::dogs::adopt) by
/// `main`'s own dispatch, never handled by `enable` itself: a dog already
/// built into this binary has no path to vet, so `enable` cannot carry out
/// what `adopt` does.
///
/// `name` here is a required positional, unlike [`AdoptArgs`]'s own
/// (optional, defaulted from the binary's file stem) — pm2's own spelling
/// carries no such default, and `enable --exec` exists only to keep that
/// spelling working verbatim, not to gain `adopt`'s newer conveniences. A
/// reader relying on the two shapes agreeing is a mistake nothing but a
/// test catches — see `main.rs`'s
/// `the_hidden_pm2_spelling_reaches_adopt_with_the_arguments_the_right_way_round`.
#[derive(Debug, clap::Args)]
pub struct EnableArgs {
    /// A sheep's name, `fold:<fold>`, `/<regex>/` or `all`, or a dog's name
    /// (the `[<name>]` config key in `dogs.toml`)
    pub name: String,
    /// Hidden pm2-spelling alias for `shep adopt`: routes to `adopt` with
    /// this flag's value as the binary path
    #[arg(long, hide = true)]
    pub exec: Option<PathBuf>,
    /// Grant a dog everything it declares without asking; a dog's first
    /// enable asks, and with no terminal and no `--yes` refuses
    #[arg(long)]
    pub yes: bool,
}

/// Arguments to `shep disable`.
#[derive(Debug, clap::Args)]
pub struct DisableArgs {
    /// A sheep's name, `fold:<fold>`, `/<regex>/` or `all`, or a dog's name
    /// (the `[<name>]` config key in `dogs.toml`)
    pub name: String,
}

/// Arguments to `shep adopt`.
///
/// `path` is the one positional; `--name` is optional and defaults to the
/// binary's file stem with a leading `shep-` stripped, the way `cargo`
/// strips `cargo-` from its own external subcommands (`shep-log-rotate`
/// defaults to `log-rotate`). Previously both were required positionals,
/// name first (`adopt <name> <path>`) — a breaking CLI change, decision
/// The maintainer: `shep adopt <path>` alone now works for a binary whose name is
/// already the name you want, matching `shep start <script>`'s own
/// optional `--name`.
#[derive(Debug, clap::Args)]
pub struct AdoptArgs {
    /// Path to the dog's binary, vetted before `shep.toml` is touched.
    /// Resolved before vetting: as given, with a leading `~/` expanded, or
    /// looked up on `$PATH` if it names no directory — first hit wins.
    pub path: PathBuf,
    /// The dog's name, the `[<name>]` config key in `dogs.toml`. Defaults
    /// to the binary's file stem with a leading `shep-` stripped.
    #[arg(long)]
    pub name: Option<String>,
    /// Grant the dog everything it declares without asking. With no
    /// terminal and no `--yes`, adopt refuses rather than granting.
    #[arg(long)]
    pub yes: bool,
}

#[cfg(test)]
mod tests {
    use crate::cli::{Cli, Commands};

    /// fails if `Commands::Dog` is wired to another verb, or if it is not
    /// hidden. It is a re-exec target, not something an operator runs.
    #[test]
    fn the_dog_subcommand_parses_and_stays_hidden() {
        use clap::{CommandFactory, Parser};

        let parsed = Cli::try_parse_from(["shep", "dog", "metrics"])
            .unwrap()
            .command;
        let Commands::Dog(args) = parsed else {
            panic!("expected dog")
        };
        assert_eq!(args.name, "metrics");

        let cmd = Cli::command();
        assert!(
            cmd.find_subcommand("dog").unwrap().is_hide_set(),
            "dog must stay hidden from --help"
        );
    }

    /// Fails if `enable`'s pm2-spelled `--exec` alias loses its `hide =
    /// true` and starts teaching itself in `--help` — the whole reason it
    /// is an argument-level hide rather than a documented flag: `shep
    /// adopt` is the verb the help text should point an operator at.
    #[test]
    fn the_exec_alias_stays_hidden_from_help() {
        use clap::CommandFactory;
        let cmd = Cli::command();
        let enable = cmd.find_subcommand("enable").unwrap();
        let exec_arg = enable
            .get_arguments()
            .find(|a| a.get_id().as_str() == "exec")
            .expect("EnableArgs must still carry a hidden `exec` field");
        assert!(
            exec_arg.is_hide_set(),
            "--exec must stay hidden from --help"
        );
    }
}