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