1use crate::commands::{
2 AdoptMigration, ApplyMigration, BuildMigration, BuildTest, Command, CompareTests, ExpectTest,
3 Init, MigrationStatus, NewMigration, NewTest, Outcome, PinMigration, RunTest,
4 TelemetryDescribe, TelemetryInfo,
5};
6use crate::config::Config;
7use opendal::Operator;
8
9use anyhow::{anyhow, Result};
10use clap::{Parser, Subcommand};
11
12#[derive(Parser)]
13#[command(version, about, long_about = None)]
14pub struct Cli {
15 #[arg(short, long)]
17 pub debug: bool,
18
19 #[arg(global = true, short, long, default_value = "spawn.toml")]
20 pub config_file: String,
21
22 #[arg(global = true, long)]
23 pub database: Option<String>,
24
25 #[arg(long, hide = true)]
27 pub internal_telemetry: bool,
28
29 #[command(subcommand)]
30 pub command: Option<Commands>,
31}
32
33impl TelemetryDescribe for Cli {
34 fn telemetry(&self) -> TelemetryInfo {
35 match &self.command {
36 Some(cmd) => cmd.telemetry(),
37 None => TelemetryInfo::default(),
38 }
39 }
40}
41
42#[derive(Subcommand)]
43pub enum Commands {
44 Init,
46 Migration {
47 #[command(subcommand)]
48 command: Option<MigrationCommands>,
49 #[arg(short, long, global = true)]
50 environment: Option<String>,
51 },
52 Test {
53 #[command(subcommand)]
54 command: Option<TestCommands>,
55 },
56}
57
58impl TelemetryDescribe for Commands {
59 fn telemetry(&self) -> TelemetryInfo {
60 match self {
61 Commands::Init => TelemetryInfo::new("init"),
62 Commands::Migration { command, .. } => match command {
63 Some(cmd) => {
64 let mut info = cmd.telemetry();
65 info.label = format!("migration {}", info.label);
66 info
67 }
68 None => TelemetryInfo::new("migration"),
69 },
70 Commands::Test { command } => match command {
71 Some(cmd) => {
72 let mut info = cmd.telemetry();
73 info.label = format!("test {}", info.label);
74 info
75 }
76 None => TelemetryInfo::new("test"),
77 },
78 }
79 }
80}
81
82#[derive(Subcommand)]
83pub enum MigrationCommands {
84 New {
86 name: String,
88 },
89 Pin {
91 migration: String,
93 },
94 Build {
96 #[arg(long)]
98 pinned: bool,
99 migration: String,
102 #[arg(long)]
105 variables: Option<String>,
106 },
107 Apply {
110 #[arg(long)]
112 no_pin: bool,
113
114 migration: Option<String>,
115
116 #[arg(long)]
119 variables: Option<String>,
120
121 #[arg(long)]
123 yes: bool,
124
125 #[arg(long)]
127 retry: bool,
128 },
129 Adopt {
132 migration: Option<String>,
134
135 #[arg(long)]
137 yes: bool,
138
139 #[arg(long)]
141 description: Option<String>,
142 },
143 Status,
145}
146
147impl TelemetryDescribe for MigrationCommands {
148 fn telemetry(&self) -> TelemetryInfo {
149 match self {
150 MigrationCommands::New { .. } => TelemetryInfo::new("new"),
151 MigrationCommands::Pin { .. } => TelemetryInfo::new("pin"),
152 MigrationCommands::Build {
153 pinned, variables, ..
154 } => TelemetryInfo::new("build").with_properties(vec![
155 ("opt_pinned", pinned.to_string()),
156 ("has_variables", variables.is_some().to_string()),
157 ]),
158 MigrationCommands::Apply {
159 no_pin,
160 variables,
161 migration,
162 retry,
163 ..
164 } => TelemetryInfo::new("apply").with_properties(vec![
165 ("opt_no_pin", no_pin.to_string()),
166 ("opt_retry", retry.to_string()),
167 ("has_variables", variables.is_some().to_string()),
168 ("apply_all", migration.is_none().to_string()),
169 ]),
170 MigrationCommands::Adopt { .. } => TelemetryInfo::new("adopt"),
171 MigrationCommands::Status => TelemetryInfo::new("status"),
172 }
173 }
174}
175
176#[derive(Subcommand)]
177pub enum TestCommands {
178 New {
180 name: String,
182 },
183 Build {
184 name: String,
185 },
186 Run {
188 name: String,
189 },
190 Compare {
192 name: Option<String>,
193 },
194 Expect {
195 name: String,
196 },
197}
198
199impl TelemetryDescribe for TestCommands {
200 fn telemetry(&self) -> TelemetryInfo {
201 match self {
202 TestCommands::New { .. } => TelemetryInfo::new("new"),
203 TestCommands::Build { .. } => TelemetryInfo::new("build"),
204 TestCommands::Run { .. } => TelemetryInfo::new("run"),
205 TestCommands::Compare { name } => TelemetryInfo::new("compare")
206 .with_properties(vec![("compare_all", name.is_none().to_string())]),
207 TestCommands::Expect { .. } => TelemetryInfo::new("expect"),
208 }
209 }
210}
211
212pub struct CliResult {
214 pub outcome: Result<Outcome>,
215 pub project_id: Option<String>,
217 pub telemetry_enabled: bool,
219}
220
221pub async fn run_cli(cli: Cli, base_op: &Operator) -> CliResult {
222 if let Some(Commands::Init) = &cli.command {
224 let init_cmd = Init {
225 config_file: cli.config_file.clone(),
226 };
227 match init_cmd.execute(base_op).await {
228 Ok((outcome, project_id)) => {
229 return CliResult {
230 outcome: Ok(outcome),
231 project_id: Some(project_id),
232 telemetry_enabled: true,
233 };
234 }
235 Err(e) => {
236 return CliResult {
237 outcome: Err(e),
238 project_id: None,
239 telemetry_enabled: true,
240 };
241 }
242 }
243 }
244
245 let config_exists = base_op.exists(&cli.config_file).await.unwrap_or(false);
247
248 let mut main_config = match Config::load(&cli.config_file, base_op, cli.database.clone()).await
250 {
251 Ok(cfg) => cfg,
252 Err(e) => {
253 if !config_exists {
255 crate::show_telemetry_notice();
256 eprintln!("No spawn.toml configuration file found.");
257 eprintln!("Run `spawn init` to create a new spawn project.");
258 return CliResult {
259 outcome: Err(anyhow!("Configuration file not found")),
260 project_id: None,
261 telemetry_enabled: false,
262 };
263 }
264
265 return CliResult {
266 outcome: Err(e.context(format!("could not load config from {}", &cli.config_file))),
267 project_id: None,
268 telemetry_enabled: false, };
270 }
271 };
272
273 let project_id = main_config.project_id.clone();
275 let telemetry_enabled = main_config.telemetry;
276
277 let outcome = run_command(cli, &mut main_config).await;
279
280 CliResult {
281 outcome,
282 project_id,
283 telemetry_enabled,
284 }
285}
286
287async fn run_command(cli: Cli, config: &mut Config) -> Result<Outcome> {
288 match cli.command {
289 Some(Commands::Init) => unreachable!(), Some(Commands::Migration {
291 command,
292 environment,
293 }) => {
294 config.environment = environment;
295 match command {
296 Some(MigrationCommands::New { name }) => {
297 NewMigration { name }.execute(config).await
298 }
299 Some(MigrationCommands::Pin { migration }) => {
300 PinMigration { migration }.execute(config).await
301 }
302 Some(MigrationCommands::Build {
303 migration,
304 pinned,
305 variables,
306 }) => {
307 let vars = match variables {
308 Some(vars_path) => Some(config.load_variables_from_path(&vars_path).await?),
309 None => None,
310 };
311 BuildMigration {
312 migration,
313 pinned,
314 variables: vars,
315 }
316 .execute(config)
317 .await
318 }
319 Some(MigrationCommands::Apply {
320 migration,
321 no_pin,
322 variables,
323 yes,
324 retry,
325 }) => {
326 let vars = match variables {
327 Some(vars_path) => Some(config.load_variables_from_path(&vars_path).await?),
328 None => None,
329 };
330 ApplyMigration {
331 migration,
332 pinned: !no_pin,
333 variables: vars,
334 yes,
335 retry,
336 }
337 .execute(config)
338 .await
339 }
340 Some(MigrationCommands::Adopt {
341 migration,
342 yes,
343 description,
344 }) => {
345 AdoptMigration {
346 migration,
347 yes,
348 description,
349 }
350 .execute(config)
351 .await
352 }
353 Some(MigrationCommands::Status) => MigrationStatus.execute(config).await,
354 None => {
355 eprintln!("No migration subcommand specified");
356 Ok(Outcome::Unimplemented)
357 }
358 }
359 }
360 Some(Commands::Test { command }) => match command {
361 Some(TestCommands::New { name }) => NewTest { name }.execute(config).await,
362 Some(TestCommands::Build { name }) => BuildTest { name }.execute(config).await,
363 Some(TestCommands::Run { name }) => RunTest { name }.execute(config).await,
364 Some(TestCommands::Compare { name }) => CompareTests { name }.execute(config).await,
365 Some(TestCommands::Expect { name }) => ExpectTest { name }.execute(config).await,
366 None => {
367 eprintln!("No test subcommand specified");
368 Ok(Outcome::Unimplemented)
369 }
370 },
371 None => Ok(Outcome::Unimplemented),
372 }
373}