1use std::{env, process, str::FromStr};
4
5use clap::{CommandFactory, Parser};
6use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
7
8use crate::cli::{
9 Cli, Commands, FederationCommands, IntrospectCommands, MigrateCommands, ValidateCommands,
10};
11
12#[allow(clippy::cognitive_complexity)] pub async fn run() {
15 use crate::{commands, output};
16
17 if let Some(code) = handle_introspection_flags() {
18 process::exit(code);
19 }
20
21 let cli = Cli::parse();
22
23 init_logging(cli.verbose, cli.debug);
24
25 let result = match cli.command {
26 Commands::Compile {
27 input,
28 types,
29 schema_dir,
30 type_files,
31 query_files,
32 mutation_files,
33 output,
34 check,
35 database,
36 } => {
37 commands::compile::run(
38 &input,
39 types.as_deref(),
40 schema_dir.as_deref(),
41 type_files,
42 query_files,
43 mutation_files,
44 &output,
45 check,
46 database.as_deref(),
47 )
48 .await
49 },
50
51 Commands::Extract {
52 input,
53 language,
54 recursive,
55 output,
56 } => commands::extract::run(&input, language.as_deref(), recursive, &output),
57
58 Commands::Explain { query } => match commands::explain::run(&query) {
59 Ok(result) => {
60 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
61 Ok(())
62 },
63 Err(e) => Err(e),
64 },
65
66 Commands::Cost { query } => match commands::cost::run(&query) {
67 Ok(result) => {
68 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
69 Ok(())
70 },
71 Err(e) => Err(e),
72 },
73
74 Commands::Analyze { schema } => match commands::analyze::run(&schema) {
75 Ok(result) => {
76 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
77 Ok(())
78 },
79 Err(e) => Err(e),
80 },
81
82 Commands::DependencyGraph { schema, format } => {
83 match commands::dependency_graph::GraphFormat::from_str(&format) {
84 Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
85 Ok(result) => {
86 println!(
87 "{}",
88 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
89 );
90 Ok(())
91 },
92 Err(e) => Err(e),
93 },
94 Err(e) => Err(anyhow::anyhow!(e)),
95 }
96 },
97
98 Commands::Lint {
99 schema,
100 federation,
101 cost,
102 cache,
103 auth,
104 compilation,
105 fail_on_critical,
106 fail_on_warning,
107 verbose: _,
108 } => {
109 let opts = commands::lint::LintOptions {
110 fail_on_critical,
111 fail_on_warning,
112 filter: commands::lint::LintCategoryFilter {
113 federation,
114 cost,
115 cache,
116 auth,
117 compilation,
118 },
119 };
120 match commands::lint::run(&schema, opts) {
121 Ok(result) => {
122 println!(
123 "{}",
124 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
125 );
126 Ok(())
127 },
128 Err(e) => Err(e),
129 }
130 },
131
132 Commands::Federation { command } => match command {
133 FederationCommands::Graph { schema, format } => {
134 match commands::federation::graph::GraphFormat::from_str(&format) {
135 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
136 Ok(result) => {
137 println!(
138 "{}",
139 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
140 );
141 Ok(())
142 },
143 Err(e) => Err(e),
144 },
145 Err(e) => Err(anyhow::anyhow!(e)),
146 }
147 },
148 },
149
150 Commands::GenerateViews {
151 schema,
152 entity,
153 view,
154 refresh_strategy,
155 output,
156 include_composition_views,
157 include_monitoring,
158 validate,
159 gen_verbose,
160 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
161 Ok(refresh_strat) => {
162 let config = commands::generate_views::GenerateViewsConfig {
163 schema_path: schema,
164 entity,
165 view,
166 refresh_strategy: refresh_strat,
167 output,
168 include_composition_views,
169 include_monitoring,
170 validate_only: validate,
171 verbose: cli.verbose || gen_verbose,
172 };
173
174 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
175 commands::generate_views::run(config, &formatter)
176 },
177 Err(e) => Err(anyhow::anyhow!(e)),
178 },
179
180 Commands::Validate {
181 command,
182 input,
183 check_cycles,
184 check_unused,
185 strict,
186 types,
187 } => match command {
188 Some(ValidateCommands::Facts { schema, database }) => {
189 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
190 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
191 .await
192 },
193 None => match input {
194 Some(input) => {
195 let opts = commands::validate::ValidateOptions {
196 check_cycles,
197 check_unused,
198 strict,
199 filter_types: types,
200 };
201 match commands::validate::run_with_options(&input, opts) {
202 Ok(result) => {
203 println!(
204 "{}",
205 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
206 );
207 if result.status == "validation-failed" {
208 Err(anyhow::anyhow!("Validation failed"))
209 } else {
210 Ok(())
211 }
212 },
213 Err(e) => Err(e),
214 }
215 },
216 None => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
217 },
218 },
219
220 Commands::Introspect { command } => match command {
221 IntrospectCommands::Facts { database, format } => {
222 match commands::introspect_facts::OutputFormat::parse(&format) {
223 Ok(fmt) => {
224 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
225 commands::introspect_facts::run(&database, fmt, &formatter).await
226 },
227 Err(e) => Err(anyhow::anyhow!(e)),
228 }
229 },
230 },
231
232 Commands::Generate {
233 input,
234 language,
235 output,
236 } => match commands::init::Language::from_str(&language) {
237 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
238 Err(e) => Err(anyhow::anyhow!(e)),
239 },
240
241 Commands::Init {
242 project_name,
243 language,
244 database,
245 size,
246 no_git,
247 } => {
248 match (
249 commands::init::Language::from_str(&language),
250 commands::init::Database::from_str(&database),
251 commands::init::ProjectSize::from_str(&size),
252 ) {
253 (Ok(lang), Ok(db), Ok(sz)) => {
254 let config = commands::init::InitConfig {
255 project_name,
256 language: lang,
257 database: db,
258 size: sz,
259 no_git,
260 };
261 commands::init::run(&config)
262 },
263 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
264 }
265 },
266
267 Commands::Migrate { command } => {
268 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
269 match command {
270 MigrateCommands::Up { database, dir } => {
271 let db_url = commands::migrate::resolve_database_url(database.as_deref());
272 match db_url {
273 Ok(url) => {
274 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
275 let action = commands::migrate::MigrateAction::Up {
276 database_url: url,
277 dir: mig_dir,
278 };
279 commands::migrate::run(&action, &formatter)
280 },
281 Err(e) => Err(e),
282 }
283 },
284 MigrateCommands::Down {
285 database,
286 dir,
287 steps,
288 } => {
289 let db_url = commands::migrate::resolve_database_url(database.as_deref());
290 match db_url {
291 Ok(url) => {
292 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
293 let action = commands::migrate::MigrateAction::Down {
294 database_url: url,
295 dir: mig_dir,
296 steps,
297 };
298 commands::migrate::run(&action, &formatter)
299 },
300 Err(e) => Err(e),
301 }
302 },
303 MigrateCommands::Status { database, dir } => {
304 let db_url = commands::migrate::resolve_database_url(database.as_deref());
305 match db_url {
306 Ok(url) => {
307 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
308 let action = commands::migrate::MigrateAction::Status {
309 database_url: url,
310 dir: mig_dir,
311 };
312 commands::migrate::run(&action, &formatter)
313 },
314 Err(e) => Err(e),
315 }
316 },
317 MigrateCommands::Create { name, dir } => {
318 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
319 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
320 commands::migrate::run(&action, &formatter)
321 },
322 }
323 },
324
325 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
326 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
327 Err(e) => Err(anyhow::anyhow!(e)),
328 },
329
330 #[cfg(feature = "run-server")]
331 Commands::Run {
332 input,
333 database,
334 port,
335 bind,
336 watch,
337 introspection,
338 } => commands::run::run(input.as_deref(), database, port, bind, watch, introspection).await,
339
340 Commands::ValidateDocuments { manifest } => {
341 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
342 match commands::validate_documents::run(&manifest, &formatter) {
343 Ok(true) => Ok(()),
344 Ok(false) => {
345 process::exit(2);
346 },
347 Err(e) => Err(e),
348 }
349 },
350
351 Commands::Serve { schema, port } => commands::serve::run(&schema, port).await,
352
353 Commands::Doctor {
354 config,
355 schema,
356 db_url,
357 json: json_flag,
358 } => {
359 let all_passed = commands::doctor::run(&config, &schema, db_url.as_deref(), json_flag);
360 if all_passed {
361 Ok(())
362 } else {
363 process::exit(1);
364 }
365 },
366 };
367
368 if let Err(e) = result {
369 eprintln!("Error: {e}");
370 if cli.debug {
371 eprintln!("\nDebug info:");
372 eprintln!("{e:?}");
373 }
374 process::exit(1);
375 }
376}
377
378fn init_logging(verbose: bool, debug: bool) {
379 let filter = if debug {
380 "fraiseql=debug,fraiseql_core=debug"
381 } else if verbose {
382 "fraiseql=info,fraiseql_core=info"
383 } else {
384 "fraiseql=warn,fraiseql_core=warn"
385 };
386
387 tracing_subscriber::registry()
388 .with(
389 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
390 )
391 .with(tracing_subscriber::fmt::layer())
392 .init();
393}
394
395fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
397 serde_json::to_value(value).unwrap_or_else(|e| {
398 eprintln!("fraiseql: failed to serialize {context}: {e}");
399 std::process::exit(2);
400 })
401}
402
403fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
405 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
406 eprintln!("fraiseql: failed to format {context}: {e}");
407 std::process::exit(2);
408 })
409}
410
411fn handle_introspection_flags() -> Option<i32> {
412 let args: Vec<String> = env::args().collect();
413
414 if args.iter().any(|a| a == "--help-json") {
415 let cmd = Cli::command();
416 let version = env!("CARGO_PKG_VERSION");
417 let help = crate::introspection::extract_cli_help(&cmd, version);
418 let result =
419 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
420 println!("{}", pretty_or_exit(&result, "command result"));
421 return Some(0);
422 }
423
424 if args.iter().any(|a| a == "--list-commands") {
425 let cmd = Cli::command();
426 let commands = crate::introspection::list_commands(&cmd);
427 let result = crate::output::CommandResult::success(
428 "list-commands",
429 serialize_or_exit(&commands, "command list"),
430 );
431 println!("{}", pretty_or_exit(&result, "command result"));
432 return Some(0);
433 }
434
435 let idx = args.iter().position(|a| a == "--show-output-schema")?;
436 let available = crate::output_schemas::list_schema_commands().join(", ");
437
438 let Some(cmd_name) = args.get(idx + 1) else {
439 let result = crate::output::CommandResult::error(
440 "show-output-schema",
441 &format!("Missing command name. Available: {available}"),
442 "MISSING_ARGUMENT",
443 );
444 println!("{}", pretty_or_exit(&result, "command result"));
445 return Some(1);
446 };
447
448 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
449 let result = crate::output::CommandResult::success(
450 "show-output-schema",
451 serialize_or_exit(&schema, "output schema"),
452 );
453 println!("{}", pretty_or_exit(&result, "command result"));
454 return Some(0);
455 }
456
457 let result = crate::output::CommandResult::error(
458 "show-output-schema",
459 &format!("Unknown command: {cmd_name}. Available: {available}"),
460 "UNKNOWN_COMMAND",
461 );
462 println!("{}", pretty_or_exit(&result, "command result"));
463 Some(1)
464}