1use crate::bundle_api::{
8 BundleCapability, BundleId, BundleIdCapabilityCreateRequestDataAttributes, BundleIdPlatform,
9};
10use crate::certs_api::{self, Certificate, CertificateType};
11use crate::device_api::Device;
12use crate::profile_api::{Profile, ProfileType};
13use crate::{AppStoreConnectClient, UnifiedApiKey};
14use anyhow::Result;
15use base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine};
16use clap::{Parser, Subcommand};
17use std::io::Write;
18use std::path::{Path, PathBuf};
19
20#[derive(Parser)]
21#[clap(author, version, about, long_about = None)]
22pub struct Args {
23 #[clap(long, global = true)]
25 pub api_key: Option<PathBuf>,
26 #[clap(subcommand)]
27 pub command: Commands,
28}
29
30#[derive(Subcommand)]
31pub enum Commands {
32 GenerateSigningCertificate {
34 #[clap(long)]
36 r#type: CertificateType,
37 pem: PathBuf,
39 },
40 CreateApiKey {
42 #[clap(long)]
44 issuer_id: String,
45 #[clap(long)]
47 key_id: String,
48 private_key: PathBuf,
50 },
51 Bundle {
52 #[clap(subcommand)]
53 command: BundleCommand,
54 },
55 Certificate {
56 #[clap(subcommand)]
57 command: CertificateCommand,
58 },
59 Device {
60 #[clap(subcommand)]
61 command: DeviceCommand,
62 },
63 Profile {
64 #[clap(subcommand)]
65 command: ProfileCommand,
66 },
67}
68
69impl Commands {
70 pub fn run(self, api_key: &Path) -> Result<()> {
71 match self {
72 Self::GenerateSigningCertificate { r#type, pem } => {
73 certs_api::generate_signing_certificate(api_key, r#type, &pem)?;
74 }
75 Self::CreateApiKey {
76 issuer_id,
77 key_id,
78 private_key,
79 } => {
80 UnifiedApiKey::from_ecdsa_pem_path(issuer_id, key_id, private_key)?
81 .write_json_file(api_key)?;
82 }
83 Self::Bundle { command } => command.run(api_key)?,
84 Self::Certificate { command } => command.run(api_key)?,
85 Self::Device { command } => command.run(api_key)?,
86 Self::Profile { command } => command.run(api_key)?,
87 }
88 Ok(())
89 }
90}
91
92#[derive(Subcommand)]
93pub enum BundleCommand {
94 Register {
95 #[clap(long)]
97 identifier: String,
98 #[clap(long)]
100 name: String,
101 },
102 List,
103 Get {
104 id: String,
106 },
107 GetProfiles {
108 id: String,
110 },
111 GetCapabilities {
112 id: String,
114 },
115 EnableCapability {
116 id: String,
118 capability: String,
120 },
121 Delete {
122 id: String,
124 },
125}
126
127impl BundleCommand {
128 pub fn run(self, api_key: &Path) -> Result<()> {
129 let client = AppStoreConnectClient::from_json_path(api_key)?;
130 match self {
131 Self::Register { identifier, name } => {
132 let resp = client.register_bundle_id(&identifier, &name)?;
133 print_bundle_id_header();
134 print_bundle_id(&resp.data);
135 }
136 Self::List => {
137 let resp = client.list_bundle_ids()?;
138 print_bundle_id_header();
139 for bundle_id in &resp.data {
140 print_bundle_id(bundle_id);
141 }
142 }
143 Self::Get { id } => {
144 let resp = client.get_bundle_id(&id)?;
145 print_bundle_id_header();
146 print_bundle_id(&resp.data);
147 }
148 Self::GetCapabilities { id } => {
149 let resp = client.list_bundle_capabilities(&id)?;
150 print_capability_header();
151 for capability in resp.data {
152 print_capability(&capability);
153 }
154 }
155 Self::EnableCapability { id, capability } => {
156 client.enable_bundle_id_capability(
157 &id,
158 BundleIdCapabilityCreateRequestDataAttributes {
159 capability_type: capability.clone(),
160 },
161 )?;
162 println!("capability {capability} enabled for bundle ID {id}");
163 }
164 Self::GetProfiles { id } => {
165 let resp = client.list_bundle_profiles(&id)?;
166 print_profile_header();
167 for profile in &resp.data {
168 print_profile(profile);
169 }
170 }
171 Self::Delete { id } => {
172 client.delete_bundle_id(&id)?;
173 }
174 }
175 Ok(())
176 }
177}
178
179fn print_bundle_id_header() {
180 println!("{: <10} | {: <20} | {: <30}", "id", "name", "identifier");
181}
182
183fn print_bundle_id(bundle_id: &BundleId) {
184 println!(
185 "{: <10} | {: <20} | {: <30}",
186 bundle_id.id, bundle_id.attributes.name, bundle_id.attributes.identifier,
187 );
188}
189
190fn print_capability_header() {
191 println!("{: <10} | {: <20}", "id", "type");
192}
193
194fn print_capability(capability: &BundleCapability) {
195 println!(
196 "{: <10} | {: <20}",
197 capability.id, capability.attributes.capability_type
198 );
199}
200
201#[derive(Subcommand)]
202pub enum CertificateCommand {
203 Create {
204 #[clap(long)]
206 r#type: CertificateType,
207 csr: PathBuf,
209 },
210 List,
211 Get {
212 id: String,
214 },
215 Revoke {
216 id: String,
218 },
219}
220
221impl CertificateCommand {
222 pub fn run(self, api_key: &Path) -> Result<()> {
223 let client = AppStoreConnectClient::from_json_path(api_key)?;
224 match self {
225 Self::Create { csr, r#type } => {
226 let csr = std::fs::read_to_string(csr)?;
227 let resp = client.create_certificate(csr, r#type)?;
228 print_certificate_header();
229 print_certificate(&resp.data);
230 }
231 Self::List => {
232 let resp = client.list_certificates()?;
233 print_certificate_header();
234 for cert in &resp.data {
235 print_certificate(cert);
236 }
237 }
238 Self::Get { id } => {
239 let resp = client.get_certificate(&id)?;
240 let cer = pem::encode(&pem::Pem::new(
241 "CERTIFICATE",
242 STANDARD_ENGINE.decode(resp.data.attributes.certificate_content)?,
243 ));
244 println!("{cer}");
245 }
246 Self::Revoke { id } => {
247 client.revoke_certificate(&id)?;
248 }
249 }
250 Ok(())
251 }
252}
253
254fn print_certificate_header() {
255 println!(
256 "{: <10} | {: <50} | {: <20}",
257 "id", "name", "expiration date"
258 );
259}
260
261fn print_certificate(cert: &Certificate) {
262 let expiration_date = cert.attributes.expiration_date.split_once('T').unwrap().0;
263 println!(
264 "{: <10} | {: <50} | {: <10}",
265 cert.id, cert.attributes.name, expiration_date
266 );
267}
268
269#[derive(Subcommand)]
270pub enum DeviceCommand {
271 Register {
272 #[clap(long)]
274 name: String,
275 #[clap(long)]
277 platform: BundleIdPlatform,
278 #[clap(long)]
280 udid: String,
281 },
282 List,
283 Get {
284 id: String,
286 },
287}
288
289impl DeviceCommand {
290 pub fn run(self, api_key: &Path) -> Result<()> {
291 let client = AppStoreConnectClient::from_json_path(api_key)?;
292 match self {
293 Self::Register {
294 name,
295 platform,
296 udid,
297 } => {
298 let resp = client.register_device(&name, platform, &udid)?;
299 print_device_header();
300 print_device(&resp.data);
301 }
302 Self::List => {
303 let resp = client.list_devices()?;
304 print_device_header();
305 for device in &resp.data {
306 print_device(device);
307 }
308 }
309 Self::Get { id } => {
310 let resp = client.get_device(&id)?;
311 print_device_header();
312 print_device(&resp.data);
313 }
314 }
315 Ok(())
316 }
317}
318
319fn print_device_header() {
320 println!(
321 "{: <10} | {: <20} | {: <20} | {: <20}",
322 "id", "name", "model", "udid"
323 );
324}
325
326fn print_device(device: &Device) {
327 let model = device.attributes.model.as_deref().unwrap_or_default();
328 println!(
329 "{: <10} | {: <20} | {: <20} | {: <20}",
330 device.id, device.attributes.name, model, device.attributes.udid,
331 );
332}
333
334#[derive(Subcommand)]
335pub enum ProfileCommand {
336 Create {
337 #[clap(long)]
339 name: String,
340 #[clap(long)]
342 profile_type: ProfileType,
343 #[clap(long)]
345 bundle_id: String,
346 #[clap(long)]
348 certificate: Vec<String>,
349 #[clap(long)]
351 device: Option<Vec<String>>,
352 },
353 List,
354 Get {
355 id: String,
357 },
358 Delete {
359 id: String,
361 },
362 GetBundleId {
363 id: String,
365 },
366 GetCertificates {
367 id: String,
369 },
370}
371
372impl ProfileCommand {
373 pub fn run(self, api_key: &Path) -> Result<()> {
374 let client = AppStoreConnectClient::from_json_path(api_key)?;
375 match self {
376 Self::Create {
377 name,
378 profile_type,
379 bundle_id,
380 certificate,
381 device,
382 } => {
383 let resp = client.create_profile(
384 &name,
385 profile_type,
386 &bundle_id,
387 &certificate,
388 device.as_deref(),
389 )?;
390 print_profile_header();
391 print_profile(&resp.data);
392 }
393 Self::List => {
394 let resp = client.list_profiles()?;
395 print_profile_header();
396 for profile in &resp.data {
397 print_profile(profile);
398 }
399 }
400 Self::Get { id } => {
401 let resp = client.get_profile(&id)?;
402 let profile = STANDARD_ENGINE.decode(resp.data.attributes.profile_content)?;
403 std::io::stdout().write_all(&profile)?;
404 }
405 Self::Delete { id } => {
406 client.delete_profile(&id)?;
407 }
408 Self::GetBundleId { id } => {
409 let bundle_id = client.get_profile_bundle_id(&id)?;
410 print_bundle_id_header();
411 print_bundle_id(&bundle_id.data);
412 }
413 Self::GetCertificates { id } => {
414 let resp = client.list_profile_certificates(&id)?;
415 print_certificate_header();
416 for cert in &resp.data {
417 print_certificate(cert);
418 }
419 }
420 }
421 Ok(())
422 }
423}
424
425fn print_profile_header() {
426 println!(
427 "{: <10} | {: <20} | {: <20} | {: <20}",
428 "id", "name", "type", "expiration date"
429 );
430}
431
432fn print_profile(profile: &Profile) {
433 let expiration_date = profile
434 .attributes
435 .expiration_date
436 .split_once('T')
437 .unwrap()
438 .0;
439 println!(
440 "{: <10} | {: <20} | {: <20} | {: <20}",
441 profile.id, profile.attributes.name, profile.attributes.profile_type, expiration_date,
442 );
443}