Skip to main content

github_app_cli/
cli.rs

1//! Clap command model — the single source of truth for parsing and completion.
2
3use std::path::PathBuf;
4
5use clap::{Args, Parser, Subcommand};
6use nils_common::cli_contract::OutputFormat;
7
8use crate::completion::CompletionShell;
9
10#[derive(Debug, Parser)]
11#[command(
12    name = "github-app-cli",
13    version,
14    long_version = nils_build_info::long_version(env!("CARGO_PKG_VERSION")),
15    about = "Mint GitHub App installation tokens for forge-cli and other tooling.",
16    long_about = "Mint short-lived GitHub App installation access tokens (and list \
17installations) so automation can act under a GitHub App bot identity. In text mode the \
18`token` command writes the raw token to stdout for capture via \
19`GH_TOKEN=$(github-app-cli token ...)`; JSON mode reports only non-secret metadata.",
20    after_help = "EXAMPLES:\n  \
21github-app-cli token --app-id 123 --installation-id 456 --key app.pem\n  \
22GH_TOKEN=\"$(github-app-cli token)\" forge-cli pr deliver ...\n  \
23github-app-cli installations --app-id 123 --key app.pem\n\
24\nENVIRONMENT:\n  \
25GITHUB_APP_ID                Default --app-id (App ID or Client ID).\n  \
26GITHUB_APP_INSTALLATION_ID   Default --installation-id.\n  \
27GITHUB_APP_PRIVATE_KEY_PATH  Default --key (path to the RSA private-key PEM).\n  \
28GITHUB_APP_PRIVATE_KEY       RSA private-key PEM contents (overrides --key).\n  \
29GITHUB_API_URL               REST API base URL (default https://api.github.com).\n\
30\nEXIT CODES:\n  \
310   success\n  \
3264  command-line usage error\n  \
3365  invalid input data (unreadable / malformed key)\n  \
3469  GitHub API or network unavailable\n  \
3570  internal software error",
36    disable_help_subcommand = true
37)]
38pub struct Cli {
39    /// Output format (defaults to text).
40    #[arg(long, global = true, value_enum)]
41    pub format: Option<OutputFormat>,
42
43    #[command(subcommand)]
44    pub command: Command,
45}
46
47impl Cli {
48    /// Resolve the effective output format.
49    pub fn output_format(&self) -> OutputFormat {
50        self.format.unwrap_or_default()
51    }
52}
53
54#[derive(Debug, Subcommand)]
55pub enum Command {
56    /// Mint an installation access token (text mode: token on stdout).
57    Token(TokenArgs),
58    /// List the App's installations and their installation IDs.
59    Installations(InstallationsArgs),
60    /// Print a shell completion script.
61    Completion(CompletionArgs),
62}
63
64/// Authentication inputs shared by the API-backed subcommands.
65#[derive(Debug, Args)]
66pub struct AppAuthArgs {
67    /// GitHub App ID or Client ID (used as the JWT issuer).
68    #[arg(long, env = "GITHUB_APP_ID", value_name = "ID")]
69    pub app_id: String,
70
71    /// Path to the App's RSA private-key PEM. Overridden by GITHUB_APP_PRIVATE_KEY.
72    #[arg(long, env = "GITHUB_APP_PRIVATE_KEY_PATH", value_name = "PATH")]
73    pub key: Option<PathBuf>,
74
75    /// GitHub REST API base URL (set for GitHub Enterprise).
76    #[arg(
77        long,
78        env = "GITHUB_API_URL",
79        default_value = "https://api.github.com",
80        value_name = "URL"
81    )]
82    pub api_url: String,
83}
84
85#[derive(Debug, Args)]
86pub struct TokenArgs {
87    #[command(flatten)]
88    pub auth: AppAuthArgs,
89
90    /// Installation ID to mint a token for (discover via `installations`).
91    #[arg(long, env = "GITHUB_APP_INSTALLATION_ID", value_name = "ID")]
92    pub installation_id: String,
93}
94
95#[derive(Debug, Args)]
96pub struct InstallationsArgs {
97    #[command(flatten)]
98    pub auth: AppAuthArgs,
99}
100
101#[derive(Debug, Args)]
102pub struct CompletionArgs {
103    /// Shell to emit a completion script for.
104    #[arg(value_enum)]
105    pub shell: CompletionShell,
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use clap::Parser;
112
113    #[test]
114    fn installations_rejects_installation_id_flag() {
115        // `--installation-id` belongs to `token` only; unknown here regardless
116        // of environment. Required auth is supplied explicitly so the failure is
117        // unambiguously the unknown flag, not a missing env-backed arg.
118        let parsed = Cli::try_parse_from([
119            "github-app-cli",
120            "installations",
121            "--app-id",
122            "1",
123            "--key",
124            "k.pem",
125            "--installation-id",
126            "2",
127        ]);
128        assert!(parsed.is_err());
129    }
130
131    #[test]
132    fn token_parses_with_all_required_args() {
133        let parsed = Cli::try_parse_from([
134            "github-app-cli",
135            "token",
136            "--app-id",
137            "1",
138            "--key",
139            "k.pem",
140            "--installation-id",
141            "2",
142        ]);
143        assert!(parsed.is_ok());
144    }
145
146    #[test]
147    fn completion_does_not_require_auth() {
148        let parsed = Cli::try_parse_from(["github-app-cli", "completion", "zsh"]);
149        assert!(parsed.is_ok());
150    }
151}