1use crate::commands::{
2 AdoptMigration, ApplyMigration, BuildMigration, BuildTest, Check, Command, CompareTests,
3 ExpectTest, 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 #[arg(long)]
49 docker: Option<Option<String>>,
50 },
51 Check,
53 Migration {
54 #[command(subcommand)]
55 command: Option<MigrationCommands>,
56 #[arg(short, long, global = true)]
57 environment: Option<String>,
58 },
59 Test {
60 #[command(subcommand)]
61 command: Option<TestCommands>,
62 },
63}
64
65impl TelemetryDescribe for Commands {
66 fn telemetry(&self) -> TelemetryInfo {
67 match self {
68 Commands::Init { .. } => TelemetryInfo::new("init"),
69 Commands::Check => TelemetryInfo::new("check"),
70 Commands::Migration { command, .. } => match command {
71 Some(cmd) => {
72 let mut info = cmd.telemetry();
73 info.label = format!("migration {}", info.label);
74 info
75 }
76 None => TelemetryInfo::new("migration"),
77 },
78 Commands::Test { command } => match command {
79 Some(cmd) => {
80 let mut info = cmd.telemetry();
81 info.label = format!("test {}", info.label);
82 info
83 }
84 None => TelemetryInfo::new("test"),
85 },
86 }
87 }
88}
89
90#[derive(Subcommand)]
91pub enum MigrationCommands {
92 New {
94 name: String,
96 },
97 Pin {
99 migration: String,
101 },
102 Build {
104 #[arg(long)]
106 pinned: bool,
107 migration: String,
110 #[arg(long)]
113 variables: Option<String>,
114 },
115 Apply {
118 #[arg(long)]
120 no_pin: bool,
121
122 migration: Option<String>,
123
124 #[arg(long)]
127 variables: Option<String>,
128
129 #[arg(long)]
131 yes: bool,
132
133 #[arg(long)]
135 retry: bool,
136 },
137 Adopt {
140 migration: Option<String>,
142
143 #[arg(long)]
145 yes: bool,
146
147 #[arg(long)]
149 description: Option<String>,
150 },
151 Status,
153}
154
155impl TelemetryDescribe for MigrationCommands {
156 fn telemetry(&self) -> TelemetryInfo {
157 match self {
158 MigrationCommands::New { .. } => TelemetryInfo::new("new"),
159 MigrationCommands::Pin { .. } => TelemetryInfo::new("pin"),
160 MigrationCommands::Build {
161 pinned, variables, ..
162 } => TelemetryInfo::new("build").with_properties(vec![
163 ("opt_pinned", pinned.to_string()),
164 ("has_variables", variables.is_some().to_string()),
165 ]),
166 MigrationCommands::Apply {
167 no_pin,
168 variables,
169 migration,
170 retry,
171 ..
172 } => TelemetryInfo::new("apply").with_properties(vec![
173 ("opt_no_pin", no_pin.to_string()),
174 ("opt_retry", retry.to_string()),
175 ("has_variables", variables.is_some().to_string()),
176 ("apply_all", migration.is_none().to_string()),
177 ]),
178 MigrationCommands::Adopt { .. } => TelemetryInfo::new("adopt"),
179 MigrationCommands::Status => TelemetryInfo::new("status"),
180 }
181 }
182}
183
184#[derive(Subcommand)]
185pub enum TestCommands {
186 New {
188 name: String,
190 },
191 Build {
192 name: String,
193 },
194 Run {
196 name: Option<String>,
197 },
198 Compare {
200 name: Option<String>,
201 },
202 Expect {
203 name: String,
204 },
205}
206
207impl TelemetryDescribe for TestCommands {
208 fn telemetry(&self) -> TelemetryInfo {
209 match self {
210 TestCommands::New { .. } => TelemetryInfo::new("new"),
211 TestCommands::Build { .. } => TelemetryInfo::new("build"),
212 TestCommands::Run { name } => TelemetryInfo::new("run")
213 .with_properties(vec![("run_all", name.is_none().to_string())]),
214 TestCommands::Compare { name } => TelemetryInfo::new("compare")
215 .with_properties(vec![("compare_all", name.is_none().to_string())]),
216 TestCommands::Expect { .. } => TelemetryInfo::new("expect"),
217 }
218 }
219}
220
221pub struct CliResult {
223 pub outcome: Result<Outcome>,
224 pub project_id: Option<String>,
226 pub telemetry_enabled: bool,
228}
229
230pub async fn run_cli(cli: Cli, base_op: &Operator) -> CliResult {
231 if let Some(Commands::Init { docker }) = &cli.command {
233 let init_cmd = Init {
234 config_file: cli.config_file.clone(),
235 docker: docker.clone(),
236 };
237 match init_cmd.execute(base_op).await {
238 Ok((outcome, project_id)) => {
239 return CliResult {
240 outcome: Ok(outcome),
241 project_id: Some(project_id),
242 telemetry_enabled: true,
243 };
244 }
245 Err(e) => {
246 return CliResult {
247 outcome: Err(e),
248 project_id: None,
249 telemetry_enabled: true,
250 };
251 }
252 }
253 }
254
255 let config_exists = base_op.exists(&cli.config_file).await.unwrap_or(false);
257
258 let mut main_config = match Config::load(&cli.config_file, base_op, cli.database.clone()).await
260 {
261 Ok(cfg) => cfg,
262 Err(e) => {
263 if !config_exists {
265 crate::show_telemetry_notice();
266 eprintln!("No spawn.toml configuration file found.");
267 eprintln!("Run `spawn init` to create a new spawn project.");
268 return CliResult {
269 outcome: Err(anyhow!("Configuration file not found")),
270 project_id: None,
271 telemetry_enabled: false,
272 };
273 }
274
275 return CliResult {
276 outcome: Err(e.context(format!("could not load config from {}", &cli.config_file))),
277 project_id: None,
278 telemetry_enabled: false, };
280 }
281 };
282
283 let project_id = main_config.project_id.clone();
285 let telemetry_enabled = main_config.telemetry;
286
287 let outcome = run_command(cli, &mut main_config).await;
289
290 CliResult {
291 outcome,
292 project_id,
293 telemetry_enabled,
294 }
295}
296
297async fn run_command(cli: Cli, config: &mut Config) -> Result<Outcome> {
298 match cli.command {
299 Some(Commands::Init { .. }) => unreachable!(), Some(Commands::Check) => Check.execute(config).await,
301 Some(Commands::Migration {
302 command,
303 environment,
304 }) => {
305 config.environment = environment;
306 match command {
307 Some(MigrationCommands::New { name }) => {
308 NewMigration { name }.execute(config).await
309 }
310 Some(MigrationCommands::Pin { migration }) => {
311 PinMigration { migration }.execute(config).await
312 }
313 Some(MigrationCommands::Build {
314 migration,
315 pinned,
316 variables,
317 }) => {
318 let vars = match variables {
319 Some(vars_path) => Some(config.load_variables_from_path(&vars_path).await?),
320 None => None,
321 };
322 BuildMigration {
323 migration,
324 pinned,
325 variables: vars,
326 }
327 .execute(config)
328 .await
329 }
330 Some(MigrationCommands::Apply {
331 migration,
332 no_pin,
333 variables,
334 yes,
335 retry,
336 }) => {
337 let vars = match variables {
338 Some(vars_path) => Some(config.load_variables_from_path(&vars_path).await?),
339 None => None,
340 };
341 ApplyMigration {
342 migration,
343 pinned: !no_pin,
344 variables: vars,
345 yes,
346 retry,
347 }
348 .execute(config)
349 .await
350 }
351 Some(MigrationCommands::Adopt {
352 migration,
353 yes,
354 description,
355 }) => {
356 AdoptMigration {
357 migration,
358 yes,
359 description,
360 }
361 .execute(config)
362 .await
363 }
364 Some(MigrationCommands::Status) => MigrationStatus.execute(config).await,
365 None => {
366 eprintln!("No migration subcommand specified");
367 Ok(Outcome::Unimplemented)
368 }
369 }
370 }
371 Some(Commands::Test { command }) => match command {
372 Some(TestCommands::New { name }) => NewTest { name }.execute(config).await,
373 Some(TestCommands::Build { name }) => BuildTest { name }.execute(config).await,
374 Some(TestCommands::Run { name }) => RunTest { name }.execute(config).await,
375 Some(TestCommands::Compare { name }) => CompareTests { name }.execute(config).await,
376 Some(TestCommands::Expect { name }) => ExpectTest { name }.execute(config).await,
377 None => {
378 eprintln!("No test subcommand specified");
379 Ok(Outcome::Unimplemented)
380 }
381 },
382 None => Ok(Outcome::Unimplemented),
383 }
384}