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, GenerateClientCommands, IntrospectCommands, MigrateCommands,
10 PerfCommands, PerfExploreCommands, SchemaCommands, ValidateCommands,
11};
12
13#[allow(clippy::cognitive_complexity)] pub async fn run() {
16 use crate::{commands, output};
17
18 if let Some(code) = handle_introspection_flags() {
19 process::exit(code);
20 }
21
22 let cli = Cli::parse();
23
24 init_logging(cli.verbose, cli.debug);
25
26 let json_output = cli.json;
27 let debug_output = cli.debug;
28
29 let result = match cli.command {
30 Commands::Compile {
31 input,
32 types,
33 schema_dir,
34 type_files,
35 query_files,
36 mutation_files,
37 output,
38 check,
39 skip_hash,
40 database,
41 emit_ddl,
42 check_migrations,
43 allow_drift,
44 config,
45 } => {
46 commands::compile::run(
47 &input,
48 types.as_deref(),
49 schema_dir.as_deref(),
50 type_files,
51 query_files,
52 mutation_files,
53 &output,
54 check,
55 database.as_deref(),
56 emit_ddl.as_deref(),
57 check_migrations,
58 skip_hash,
59 allow_drift,
60 config.as_deref(),
61 )
62 .await
63 },
64
65 Commands::Extract {
66 input,
67 language,
68 recursive,
69 output,
70 } => commands::extract::run(&input, language.as_deref(), recursive, &output),
71
72 Commands::Explain { query } => match commands::explain::run(&query) {
73 Ok(result) => {
74 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
75 Ok(())
76 },
77 Err(e) => Err(e),
78 },
79
80 Commands::Cost { query } => match commands::cost::run(&query) {
81 Ok(result) => {
82 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
83 Ok(())
84 },
85 Err(e) => Err(e),
86 },
87
88 Commands::Analyze { schema } => match commands::analyze::run(&schema) {
89 Ok(result) => {
90 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
91 Ok(())
92 },
93 Err(e) => Err(e),
94 },
95
96 Commands::DependencyGraph { schema, format } => {
97 match commands::dependency_graph::GraphFormat::from_str(&format) {
98 Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
99 Ok(result) => {
100 println!(
101 "{}",
102 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
103 );
104 Ok(())
105 },
106 Err(e) => Err(e),
107 },
108 Err(e) => Err(anyhow::anyhow!(e)),
109 }
110 },
111
112 Commands::Lint {
113 schema,
114 federation,
115 cost,
116 cache,
117 auth,
118 compilation,
119 fail_on_critical,
120 fail_on_warning,
121 } => {
122 let opts = commands::lint::LintOptions {
123 fail_on_critical,
124 fail_on_warning,
125 filter: commands::lint::LintCategoryFilter {
126 federation,
127 cost,
128 cache,
129 auth,
130 compilation,
131 },
132 };
133 match commands::lint::run(&schema, opts) {
134 Ok(result) => {
135 println!(
136 "{}",
137 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
138 );
139 enforce_exit_code(&result);
142 Ok(())
143 },
144 Err(e) => Err(e),
145 }
146 },
147
148 Commands::Federation { command } => match command {
149 FederationCommands::Graph { schema, format } => {
150 match commands::federation::graph::GraphFormat::from_str(&format) {
151 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
152 Ok(result) => {
153 println!(
154 "{}",
155 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
156 );
157 Ok(())
158 },
159 Err(e) => Err(e),
160 },
161 Err(e) => Err(anyhow::anyhow!(e)),
162 }
163 },
164 #[cfg(feature = "federation")]
165 FederationCommands::Sdl { schema, output } => {
166 commands::federation::sdl::run(&schema, output.as_deref())
167 },
168 FederationCommands::Check {
169 schema,
170 against,
171 json,
172 } => {
173 match commands::federation::check::run(
174 &schema,
175 against.as_deref(),
176 json || cli.json,
177 ) {
178 Ok(result) => {
179 if !(json || cli.json) {
184 println!(
185 "{}",
186 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
187 );
188 }
189 enforce_exit_code(&result);
192 Ok(())
193 },
194 Err(e) => Err(e),
195 }
196 },
197 },
198
199 Commands::GenerateViews {
200 schema,
201 entity,
202 view,
203 refresh_strategy,
204 output,
205 include_composition_views,
206 include_monitoring,
207 validate,
208 gen_verbose,
209 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
210 Ok(refresh_strat) => {
211 let config = commands::generate_views::GenerateViewsConfig {
212 schema_path: schema,
213 entity,
214 view,
215 refresh_strategy: refresh_strat,
216 output,
217 include_composition_views,
218 include_monitoring,
219 validate_only: validate,
220 verbose: cli.verbose || gen_verbose,
221 };
222
223 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
224 commands::generate_views::run(config, &formatter).await
225 },
226 Err(e) => Err(anyhow::anyhow!(e)),
227 },
228
229 Commands::GenerateCaptureTriggers {
230 schema,
231 output,
232 no_function,
233 } => {
234 let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
235 schema_path: schema,
236 output,
237 include_function: !no_function,
238 };
239 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
240 commands::generate_capture_triggers::run(config, &formatter)
241 },
242
243 Commands::Validate {
244 command,
245 input,
246 check_cycles,
247 check_unused,
248 strict,
249 types,
250 against_db,
251 } => match command {
252 Some(ValidateCommands::Facts { schema, database }) => {
253 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
254 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
255 .await
256 },
257 None => match (against_db, input) {
258 (Some(db_url), Some(input)) => {
259 commands::validate::run_against_db(&input, &db_url, cli.json).await
260 },
261 (Some(_), None) => Err(anyhow::anyhow!(
262 "INPUT (schema.compiled.json) is required with --against-db"
263 )),
264 (None, Some(input)) => {
265 let opts = commands::validate::ValidateOptions {
266 check_cycles,
267 check_unused,
268 strict,
269 filter_types: types,
270 };
271 match commands::validate::run_with_options(&input, opts) {
272 Ok(result) => {
273 println!(
274 "{}",
275 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
276 );
277 enforce_exit_code(&result);
286 Ok(())
287 },
288 Err(e) => Err(e),
289 }
290 },
291 (None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
292 },
293 },
294
295 Commands::Introspect { command } => match command {
296 IntrospectCommands::Facts { database, format } => {
297 match commands::introspect_facts::OutputFormat::parse(&format) {
298 Ok(fmt) => {
299 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
300 commands::introspect_facts::run(&database, fmt, &formatter).await
301 },
302 Err(e) => Err(anyhow::anyhow!(e)),
303 }
304 },
305 },
306
307 Commands::Generate {
308 input,
309 language,
310 output,
311 } => match commands::init::Language::from_str(&language) {
312 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
313 Err(e) => Err(anyhow::anyhow!(e)),
314 },
315
316 Commands::GenerateClient { language } => match language {
317 GenerateClientCommands::Typescript { schema, out, force } => {
318 commands::generate_client::run(
319 commands::generate_client::ClientLanguage::TypeScript,
320 schema.as_deref(),
321 &out,
322 force,
323 )
324 },
325 GenerateClientCommands::Python { schema, out, force } => {
326 commands::generate_client::run(
327 commands::generate_client::ClientLanguage::Python,
328 schema.as_deref(),
329 &out,
330 force,
331 )
332 },
333 GenerateClientCommands::Go { schema, out, force } => commands::generate_client::run(
334 commands::generate_client::ClientLanguage::Go,
335 schema.as_deref(),
336 &out,
337 force,
338 ),
339 GenerateClientCommands::Rust { schema, out, force } => commands::generate_client::run(
340 commands::generate_client::ClientLanguage::Rust,
341 schema.as_deref(),
342 &out,
343 force,
344 ),
345 },
346
347 Commands::Init {
348 project_name,
349 language,
350 database,
351 size,
352 no_git,
353 } => {
354 match (
355 commands::init::Language::from_str(&language),
356 commands::init::Database::from_str(&database),
357 commands::init::ProjectSize::from_str(&size),
358 ) {
359 (Ok(lang), Ok(db), Ok(sz)) => {
360 let config = commands::init::InitConfig {
361 project_name,
362 language: lang,
363 database: db,
364 size: sz,
365 no_git,
366 };
367 commands::init::run(&config)
368 },
369 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
370 }
371 },
372
373 Commands::Migrate { command } => {
374 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
375 match command {
376 MigrateCommands::Up { database, dir } => {
377 match migrate_database(database.as_deref(), &formatter).await {
378 Ok(url) => {
379 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
380 let action = commands::migrate::MigrateAction::Up {
381 database_url: url,
382 dir: mig_dir,
383 };
384 commands::migrate::run(&action, &formatter)
385 },
386 Err(e) => Err(e),
387 }
388 },
389 MigrateCommands::Down {
390 database,
391 dir,
392 steps,
393 } => match migrate_database(database.as_deref(), &formatter).await {
394 Ok(url) => {
395 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
396 let action = commands::migrate::MigrateAction::Down {
397 database_url: url,
398 dir: mig_dir,
399 steps,
400 };
401 commands::migrate::run(&action, &formatter)
402 },
403 Err(e) => Err(e),
404 },
405 MigrateCommands::Status { database, dir } => {
406 match migrate_database(database.as_deref(), &formatter).await {
407 Ok(url) => {
408 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
409 let action = commands::migrate::MigrateAction::Status {
410 database_url: url,
411 dir: mig_dir,
412 };
413 commands::migrate::run(&action, &formatter)
414 },
415 Err(e) => Err(e),
416 }
417 },
418 MigrateCommands::Create { name, dir } => {
419 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
420 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
421 commands::migrate::run(&action, &formatter)
422 },
423 MigrateCommands::Generate { name, dir } => {
424 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
425 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
426 commands::migrate::run(&action, &formatter)
427 },
428 MigrateCommands::Validate { dir } => {
429 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
430 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
431 commands::migrate::run(&action, &formatter)
432 },
433 MigrateCommands::Preflight { dir } => {
434 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
435 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
436 commands::migrate::run(&action, &formatter)
437 },
438 }
439 },
440
441 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
442 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
443 Err(e) => Err(anyhow::anyhow!(e)),
444 },
445
446 #[cfg(feature = "run-server")]
447 Commands::Run {
448 input,
449 database,
450 port,
451 bind,
452 watch,
453 introspection,
454 } => {
455 Box::pin(commands::run::run(
459 input.as_deref(),
460 database,
461 port,
462 bind,
463 watch,
464 introspection,
465 ))
466 .await
467 },
468
469 Commands::Watch {
470 input,
471 output,
472 reload_url,
473 admin_token,
474 database,
475 } => {
476 commands::watch::run(
477 &input,
478 &output,
479 reload_url.as_deref(),
480 admin_token.as_deref(),
481 database.as_deref(),
482 )
483 .await
484 },
485
486 Commands::ValidateDocuments {
487 manifest,
488 max_cost,
489 schema,
490 } => {
491 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
492 match commands::validate_documents::run(
493 &manifest,
494 max_cost,
495 schema.as_deref(),
496 &formatter,
497 ) {
498 Ok(true) => Ok(()),
499 Ok(false) => {
500 process::exit(2);
501 },
502 Err(e) => Err(e),
503 }
504 },
505
506 Commands::Setup { database, dry_run } => {
507 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
508 commands::setup::run(database.as_deref(), dry_run, &formatter).await
509 },
510
511 Commands::Schema { command } => match command {
512 SchemaCommands::Metadata { server, token } => {
513 commands::schema::metadata::run(&server, token.as_deref()).await
514 },
515 },
516
517 Commands::Query {
518 query,
519 schema,
520 database,
521 variables,
522 dry_run,
523 } => {
524 Box::pin(commands::query::run(&query, &schema, database, variables, dry_run)).await
527 },
528
529 Commands::Doctor {
530 config,
531 schema,
532 db_url,
533 against_db,
534 runtime,
535 schemas,
536 json: json_flag,
537 } => {
538 let all_passed = commands::doctor::run_with_db_checks(
539 &config,
540 &schema,
541 db_url.as_deref(),
542 against_db.as_deref(),
543 runtime,
544 &schemas,
545 json_flag,
546 )
547 .await;
548 if all_passed {
549 Ok(())
550 } else {
551 process::exit(1);
552 }
553 },
554
555 Commands::Perf { command } => match command {
556 PerfCommands::RegressionScan {
557 database,
558 recent_days,
559 baseline_days,
560 min_samples,
561 threshold_pct,
562 min_delta_ms,
563 object_type,
564 fail_on_regression,
565 } => {
566 let args = commands::perf::RegressionScanArgs {
567 database,
568 recent_days,
569 baseline_days,
570 min_samples,
571 threshold_pct,
572 min_delta_ms,
573 object_type,
574 fail_on_regression,
575 json: cli.json,
576 quiet: cli.quiet,
577 };
578 match commands::perf::run_regression_scan(args).await {
579 Ok(true) => Ok(()),
580 Ok(false) => process::exit(1),
581 Err(e) => Err(e),
582 }
583 },
584 PerfCommands::Explore { command } => match command {
585 PerfExploreCommands::Slowest {
586 database,
587 days,
588 object_type,
589 limit,
590 } => {
591 commands::perf::run_explore_slowest(
592 database,
593 days,
594 object_type,
595 limit,
596 cli.json,
597 cli.quiet,
598 )
599 .await
600 },
601 PerfExploreCommands::NullRate {
602 database,
603 days,
604 object_type,
605 } => {
606 commands::perf::run_explore_null_rate(
607 database,
608 days,
609 object_type,
610 cli.json,
611 cli.quiet,
612 )
613 .await
614 },
615 PerfExploreCommands::Summary {
616 database,
617 days,
618 object_type,
619 } => {
620 commands::perf::run_explore_summary(
621 database,
622 days,
623 object_type,
624 cli.json,
625 cli.quiet,
626 )
627 .await
628 },
629 },
630 },
631
632 Commands::Sources {
633 schema,
634 db_url,
635 json,
636 } => {
637 commands::sources::run(commands::sources::SourcesArgs {
638 schema,
639 database: db_url,
640 json,
641 })
642 .await
643 },
644
645 #[cfg(feature = "functions-invoke")]
646 Commands::Functions(command) => match command {
647 crate::cli::FunctionsCommands::Invoke {
648 name,
649 payload,
650 schema,
651 mock_http,
652 mock_query,
653 idempotency_token,
654 explain,
655 } => match commands::functions::invoke(
656 &name,
657 &payload,
658 &schema,
659 mock_http.as_deref(),
660 mock_query.as_deref(),
661 idempotency_token,
662 explain,
663 cli.json,
664 )
665 .await
666 {
667 Ok(code) => process::exit(code),
669 Err(error) => Err(error),
671 },
672 },
673 };
674
675 if let Err(e) = result {
676 let debug_info = if debug_output {
677 Some(format!("{e:?}"))
678 } else {
679 None
680 };
681 let causes = error_causes(&e);
682 eprintln!(
683 "{}",
684 format_cli_error(&e.to_string(), &causes, debug_info.as_deref(), json_output, 1)
685 );
686 process::exit(1);
687 }
688}
689
690fn enforce_exit_code(result: &crate::output::CommandResult) {
703 match result.status.as_str() {
704 "success" => {},
705 "validation-failed" => process::exit(2),
706 _ => process::exit(1),
707 }
708}
709
710pub(crate) fn error_causes(err: &anyhow::Error) -> Vec<String> {
718 err.chain().skip(1).map(ToString::to_string).collect()
719}
720
721pub(crate) fn format_cli_error(
739 message: &str,
740 causes: &[String],
741 debug_info: Option<&str>,
742 json: bool,
743 code: i32,
744) -> String {
745 if json {
746 serde_json::json!({
747 "error": {
748 "message": message,
749 "causes": causes,
750 "code": code
751 }
752 })
753 .to_string()
754 } else {
755 let mut out = format!("Error: {message}");
756 for cause in causes {
757 out.push_str(&format!("\n caused by: {cause}"));
758 }
759 if let Some(debug) = debug_info {
760 out.push_str("\n\nDebug info:\n");
761 out.push_str(debug);
762 }
763 out
764 }
765}
766
767fn init_logging(verbose: bool, debug: bool) {
768 let filter = if debug {
769 "fraiseql=debug,fraiseql_core=debug"
770 } else if verbose {
771 "fraiseql=info,fraiseql_core=info"
772 } else {
773 "fraiseql=warn,fraiseql_core=warn"
774 };
775
776 tracing_subscriber::registry()
777 .with(
778 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
779 )
780 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
784 .init();
785}
786
787fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
789 serde_json::to_value(value).unwrap_or_else(|e| {
790 eprintln!("fraiseql: failed to serialize {context}: {e}");
791 std::process::exit(2);
792 })
793}
794
795fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
797 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
798 eprintln!("fraiseql: failed to format {context}: {e}");
799 std::process::exit(2);
800 })
801}
802
803fn handle_introspection_flags() -> Option<i32> {
804 let args: Vec<String> = env::args().collect();
805
806 if args.iter().any(|a| a == "--help-json") {
807 let cmd = Cli::command();
808 let version = env!("CARGO_PKG_VERSION");
809 let help = crate::introspection::extract_cli_help(&cmd, version);
810 let result =
811 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
812 println!("{}", pretty_or_exit(&result, "command result"));
813 return Some(0);
814 }
815
816 if args.iter().any(|a| a == "--list-commands") {
817 let cmd = Cli::command();
818 let commands = crate::introspection::list_commands(&cmd);
819 let result = crate::output::CommandResult::success(
820 "list-commands",
821 serialize_or_exit(&commands, "command list"),
822 );
823 println!("{}", pretty_or_exit(&result, "command result"));
824 return Some(0);
825 }
826
827 let idx = args.iter().position(|a| a == "--show-output-schema")?;
828 let available = crate::output_schemas::list_schema_commands().join(", ");
829
830 let Some(cmd_name) = args.get(idx + 1) else {
831 let result = crate::output::CommandResult::error(
832 "show-output-schema",
833 &format!("Missing command name. Available: {available}"),
834 "MISSING_ARGUMENT",
835 );
836 println!("{}", pretty_or_exit(&result, "command result"));
837 return Some(1);
838 };
839
840 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
841 let result = crate::output::CommandResult::success(
842 "show-output-schema",
843 serialize_or_exit(&schema, "output schema"),
844 );
845 println!("{}", pretty_or_exit(&result, "command result"));
846 return Some(0);
847 }
848
849 let result = crate::output::CommandResult::error(
850 "show-output-schema",
851 &format!("Unknown command: {cmd_name}. Available: {available}"),
852 "UNKNOWN_COMMAND",
853 );
854 println!("{}", pretty_or_exit(&result, "command result"));
855 Some(1)
856}
857
858async fn migrate_database(
861 explicit: Option<&str>,
862 formatter: &crate::output::OutputFormatter,
863) -> anyhow::Result<String> {
864 crate::commands::migrate::require_confiture(formatter)?;
865 let database = crate::commands::migrate::resolve_database(explicit)?;
866 crate::commands::migrate::refuse_unsupported_server(&database).await?;
867 Ok(database.url)
868}