Skip to main content

hm_plugin_cloud/
cli.rs

1//! CLI parsing for cloud subcommands.
2
3use std::collections::BTreeMap;
4
5use anyhow::Result;
6use clap::{Parser, Subcommand};
7
8use crate::{auth, verbs};
9
10#[derive(Debug, Parser)]
11#[command(
12    name = "hm cloud",
13    about = "Talk to the Harmont cloud API",
14    disable_help_subcommand = true
15)]
16struct CloudCli {
17    #[command(subcommand)]
18    command: CloudCommand,
19}
20
21#[derive(Debug, Clone, Subcommand)]
22pub enum CloudCommand {
23    /// Authenticate this CLI against the Harmont API.
24    Login {
25        /// Skip the loopback flow and prompt for a paste-in code.
26        #[arg(long)]
27        paste: bool,
28    },
29    /// Remove stored credentials.
30    Logout,
31    /// Show the authenticated user.
32    Whoami,
33    /// Manage organizations.
34    #[command(subcommand)]
35    Org(OrgCommand),
36    /// Manage pipelines.
37    #[command(subcommand)]
38    Pipeline(PipelineCommand),
39    /// Manage builds.
40    #[command(subcommand)]
41    Build(BuildCommand),
42    /// Manage jobs.
43    #[command(subcommand)]
44    Job(JobCommand),
45    /// Manage credits, top-ups, and usage.
46    #[command(subcommand)]
47    Billing(BillingCommand),
48    /// Submit the local pipeline to the cloud and watch its build.
49    Run(verbs::run::RunArgs),
50}
51
52#[derive(Debug, Clone, Subcommand)]
53pub enum OrgCommand {
54    /// Set the active organization.
55    Switch {
56        /// Organization slug.
57        slug: String,
58    },
59}
60
61#[derive(Debug, Clone, Subcommand)]
62pub enum PipelineCommand {
63    /// List pipelines for the active organization.
64    List,
65    /// Show pipeline details by slug.
66    Show { slug: String },
67}
68
69#[derive(Debug, Clone, Subcommand)]
70pub enum BuildCommand {
71    /// List builds for a pipeline.
72    List {
73        #[arg(short, long)]
74        pipeline: String,
75    },
76    /// Show a build by number.
77    Show {
78        #[arg(short, long)]
79        pipeline: String,
80        number: i64,
81    },
82    /// Cancel a build.
83    Cancel {
84        #[arg(short, long)]
85        pipeline: String,
86        number: i64,
87    },
88    /// Watch a build until it reaches a terminal state.
89    Watch {
90        #[arg(short, long)]
91        pipeline: String,
92        number: i64,
93    },
94}
95
96#[derive(Debug, Clone, Subcommand)]
97pub enum JobCommand {
98    /// List jobs in a build.
99    List {
100        #[arg(short, long)]
101        pipeline: String,
102        #[arg(short, long)]
103        build: i64,
104    },
105    /// Show a job by id.
106    Show {
107        #[arg(short, long)]
108        pipeline: String,
109        #[arg(short, long)]
110        build: i64,
111        job_id: String,
112    },
113    /// Print the job log.
114    Log {
115        #[arg(short, long)]
116        pipeline: String,
117        #[arg(short, long)]
118        build: i64,
119        job_id: String,
120    },
121}
122
123#[derive(Debug, Clone, Subcommand)]
124pub enum BillingCommand {
125    /// Print the current credit balance.
126    Balance,
127    /// List billing transactions.
128    Transactions {
129        #[arg(long, default_value = "100")]
130        limit: u32,
131    },
132    /// Show usage over a time window.
133    Usage {
134        #[arg(long)]
135        from: Option<String>,
136        #[arg(long)]
137        to: Option<String>,
138    },
139    /// Top up credits via Stripe checkout.
140    Topup {
141        amount_usd: u32,
142        #[arg(long)]
143        no_browser: bool,
144    },
145    /// Redeem a coupon code.
146    Redeem { code: String },
147}
148
149/// Dispatch from raw argv (used if calling from an external-subcommand
150/// pattern). Returns an exit code.
151pub async fn dispatch(argv: Vec<String>, env: BTreeMap<String, String>) -> Result<i32> {
152    let mut full: Vec<String> = vec!["hm cloud".to_string()];
153    full.extend(argv.into_iter().skip(1));
154    let parsed = match CloudCli::try_parse_from(&full) {
155        Ok(p) => p,
156        Err(e) => {
157            use clap::error::ErrorKind;
158            let msg = e.to_string();
159            return match e.kind() {
160                ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
161                    #[allow(clippy::print_stdout)]
162                    {
163                        use std::io::Write;
164                        std::io::stdout().write_all(msg.as_bytes()).ok();
165                    }
166                    Ok(0)
167                }
168                _ => {
169                    #[allow(clippy::print_stderr)]
170                    {
171                        use std::io::Write;
172                        std::io::stderr().write_all(msg.as_bytes()).ok();
173                    }
174                    Ok(2)
175                }
176            };
177        }
178    };
179    dispatch_command(parsed.command, env).await
180}
181
182/// Dispatch from a pre-parsed `CloudCommand`. Returns an exit code.
183pub async fn dispatch_command(command: CloudCommand, env: BTreeMap<String, String>) -> Result<i32> {
184    let result = match command {
185        CloudCommand::Login { paste } => auth::login::run(&env, paste).await,
186        CloudCommand::Logout => auth::logout::run(&env).await,
187        CloudCommand::Whoami => auth::whoami::run(&env).await,
188        CloudCommand::Org(cmd) => verbs::org::run(&env, cmd).await,
189        CloudCommand::Pipeline(cmd) => verbs::pipeline::run(&env, cmd).await,
190        CloudCommand::Build(cmd) => verbs::build::run(&env, cmd).await,
191        CloudCommand::Job(cmd) => verbs::job::run(&env, cmd).await,
192        CloudCommand::Billing(cmd) => verbs::billing::run(&env, cmd).await,
193        CloudCommand::Run(args) => verbs::run::run(&env, args).await,
194    };
195    match result {
196        Ok(()) => Ok(0),
197        Err(e) => {
198            tracing::error!("{e:#}");
199            Ok(1)
200        }
201    }
202}