1use 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 #[arg(long, global = true, value_enum)]
41 pub format: Option<OutputFormat>,
42
43 #[command(subcommand)]
44 pub command: Command,
45}
46
47impl Cli {
48 pub fn output_format(&self) -> OutputFormat {
50 self.format.unwrap_or_default()
51 }
52}
53
54#[derive(Debug, Subcommand)]
55pub enum Command {
56 Token(TokenArgs),
58 Installations(InstallationsArgs),
60 Completion(CompletionArgs),
62}
63
64#[derive(Debug, Args)]
66pub struct AppAuthArgs {
67 #[arg(long, env = "GITHUB_APP_ID", value_name = "ID")]
69 pub app_id: String,
70
71 #[arg(long, env = "GITHUB_APP_PRIVATE_KEY_PATH", value_name = "PATH")]
73 pub key: Option<PathBuf>,
74
75 #[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 #[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 #[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 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}