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 } => {
44 commands::compile::run(
45 &input,
46 types.as_deref(),
47 schema_dir.as_deref(),
48 type_files,
49 query_files,
50 mutation_files,
51 &output,
52 check,
53 database.as_deref(),
54 emit_ddl.as_deref(),
55 check_migrations,
56 skip_hash,
57 )
58 .await
59 },
60
61 Commands::Extract {
62 input,
63 language,
64 recursive,
65 output,
66 } => commands::extract::run(&input, language.as_deref(), recursive, &output),
67
68 Commands::Explain { query } => match commands::explain::run(&query) {
69 Ok(result) => {
70 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
71 Ok(())
72 },
73 Err(e) => Err(e),
74 },
75
76 Commands::Cost { query } => match commands::cost::run(&query) {
77 Ok(result) => {
78 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
79 Ok(())
80 },
81 Err(e) => Err(e),
82 },
83
84 Commands::Analyze { schema } => match commands::analyze::run(&schema) {
85 Ok(result) => {
86 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
87 Ok(())
88 },
89 Err(e) => Err(e),
90 },
91
92 Commands::DependencyGraph { schema, format } => {
93 match commands::dependency_graph::GraphFormat::from_str(&format) {
94 Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
95 Ok(result) => {
96 println!(
97 "{}",
98 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
99 );
100 Ok(())
101 },
102 Err(e) => Err(e),
103 },
104 Err(e) => Err(anyhow::anyhow!(e)),
105 }
106 },
107
108 Commands::Lint {
109 schema,
110 federation,
111 cost,
112 cache,
113 auth,
114 compilation,
115 fail_on_critical,
116 fail_on_warning,
117 verbose: _,
118 } => {
119 let opts = commands::lint::LintOptions {
120 fail_on_critical,
121 fail_on_warning,
122 filter: commands::lint::LintCategoryFilter {
123 federation,
124 cost,
125 cache,
126 auth,
127 compilation,
128 },
129 };
130 match commands::lint::run(&schema, opts) {
131 Ok(result) => {
132 println!(
133 "{}",
134 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
135 );
136 Ok(())
137 },
138 Err(e) => Err(e),
139 }
140 },
141
142 Commands::Federation { command } => match command {
143 FederationCommands::Graph { schema, format } => {
144 match commands::federation::graph::GraphFormat::from_str(&format) {
145 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
146 Ok(result) => {
147 println!(
148 "{}",
149 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
150 );
151 Ok(())
152 },
153 Err(e) => Err(e),
154 },
155 Err(e) => Err(anyhow::anyhow!(e)),
156 }
157 },
158 FederationCommands::Check {
159 schema,
160 against,
161 json,
162 } => {
163 match commands::federation::check::run(
164 &schema,
165 against.as_deref(),
166 json || cli.json,
167 ) {
168 Ok(result) => {
169 if !json {
170 println!(
171 "{}",
172 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
173 );
174 }
175 Ok(())
176 },
177 Err(e) => Err(e),
178 }
179 },
180 },
181
182 Commands::GenerateViews {
183 schema,
184 entity,
185 view,
186 refresh_strategy,
187 output,
188 include_composition_views,
189 include_monitoring,
190 validate,
191 gen_verbose,
192 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
193 Ok(refresh_strat) => {
194 let config = commands::generate_views::GenerateViewsConfig {
195 schema_path: schema,
196 entity,
197 view,
198 refresh_strategy: refresh_strat,
199 output,
200 include_composition_views,
201 include_monitoring,
202 validate_only: validate,
203 verbose: cli.verbose || gen_verbose,
204 };
205
206 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
207 commands::generate_views::run(config, &formatter)
208 },
209 Err(e) => Err(anyhow::anyhow!(e)),
210 },
211
212 Commands::GenerateCaptureTriggers {
213 schema,
214 output,
215 no_function,
216 } => {
217 let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
218 schema_path: schema,
219 output,
220 include_function: !no_function,
221 };
222 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
223 commands::generate_capture_triggers::run(config, &formatter)
224 },
225
226 Commands::Validate {
227 command,
228 input,
229 check_cycles,
230 check_unused,
231 strict,
232 types,
233 against_db,
234 } => match command {
235 Some(ValidateCommands::Facts { schema, database }) => {
236 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
237 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
238 .await
239 },
240 None => match (against_db, input) {
241 (Some(db_url), Some(input)) => {
242 commands::validate::run_against_db(&input, &db_url, cli.json).await
243 },
244 (Some(_), None) => Err(anyhow::anyhow!(
245 "INPUT (schema.compiled.json) is required with --against-db"
246 )),
247 (None, Some(input)) => {
248 let opts = commands::validate::ValidateOptions {
249 check_cycles,
250 check_unused,
251 strict,
252 filter_types: types,
253 };
254 match commands::validate::run_with_options(&input, opts) {
255 Ok(result) => {
256 println!(
257 "{}",
258 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
259 );
260 if result.status == "validation-failed" {
261 Err(anyhow::anyhow!("Validation failed"))
262 } else {
263 Ok(())
264 }
265 },
266 Err(e) => Err(e),
267 }
268 },
269 (None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
270 },
271 },
272
273 Commands::Introspect { command } => match command {
274 IntrospectCommands::Facts { database, format } => {
275 match commands::introspect_facts::OutputFormat::parse(&format) {
276 Ok(fmt) => {
277 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
278 commands::introspect_facts::run(&database, fmt, &formatter).await
279 },
280 Err(e) => Err(anyhow::anyhow!(e)),
281 }
282 },
283 },
284
285 Commands::Generate {
286 input,
287 language,
288 output,
289 } => match commands::init::Language::from_str(&language) {
290 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
291 Err(e) => Err(anyhow::anyhow!(e)),
292 },
293
294 Commands::GenerateClient { language } => match language {
295 GenerateClientCommands::Typescript { schema, out, force } => {
296 commands::generate_client::run(
297 commands::generate_client::ClientLanguage::TypeScript,
298 schema.as_deref(),
299 &out,
300 force,
301 )
302 },
303 },
304
305 Commands::Init {
306 project_name,
307 language,
308 database,
309 size,
310 no_git,
311 } => {
312 match (
313 commands::init::Language::from_str(&language),
314 commands::init::Database::from_str(&database),
315 commands::init::ProjectSize::from_str(&size),
316 ) {
317 (Ok(lang), Ok(db), Ok(sz)) => {
318 let config = commands::init::InitConfig {
319 project_name,
320 language: lang,
321 database: db,
322 size: sz,
323 no_git,
324 };
325 commands::init::run(&config)
326 },
327 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
328 }
329 },
330
331 Commands::Migrate { command } => {
332 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
333 match command {
334 MigrateCommands::Up { database, dir } => {
335 let db_url = commands::migrate::resolve_database_url(database.as_deref());
336 match db_url {
337 Ok(url) => {
338 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
339 let action = commands::migrate::MigrateAction::Up {
340 database_url: url,
341 dir: mig_dir,
342 };
343 commands::migrate::run(&action, &formatter)
344 },
345 Err(e) => Err(e),
346 }
347 },
348 MigrateCommands::Down {
349 database,
350 dir,
351 steps,
352 } => {
353 let db_url = commands::migrate::resolve_database_url(database.as_deref());
354 match db_url {
355 Ok(url) => {
356 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
357 let action = commands::migrate::MigrateAction::Down {
358 database_url: url,
359 dir: mig_dir,
360 steps,
361 };
362 commands::migrate::run(&action, &formatter)
363 },
364 Err(e) => Err(e),
365 }
366 },
367 MigrateCommands::Status { database, dir } => {
368 let db_url = commands::migrate::resolve_database_url(database.as_deref());
369 match db_url {
370 Ok(url) => {
371 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
372 let action = commands::migrate::MigrateAction::Status {
373 database_url: url,
374 dir: mig_dir,
375 };
376 commands::migrate::run(&action, &formatter)
377 },
378 Err(e) => Err(e),
379 }
380 },
381 MigrateCommands::Create { name, dir } => {
382 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
383 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
384 commands::migrate::run(&action, &formatter)
385 },
386 MigrateCommands::Generate { name, dir } => {
387 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
388 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
389 commands::migrate::run(&action, &formatter)
390 },
391 MigrateCommands::Validate { dir } => {
392 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
393 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
394 commands::migrate::run(&action, &formatter)
395 },
396 MigrateCommands::Preflight { dir } => {
397 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
398 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
399 commands::migrate::run(&action, &formatter)
400 },
401 }
402 },
403
404 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
405 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
406 Err(e) => Err(anyhow::anyhow!(e)),
407 },
408
409 #[cfg(feature = "run-server")]
410 Commands::Run {
411 input,
412 database,
413 port,
414 bind,
415 watch,
416 introspection,
417 } => {
418 Box::pin(commands::run::run(
422 input.as_deref(),
423 database,
424 port,
425 bind,
426 watch,
427 introspection,
428 ))
429 .await
430 },
431
432 Commands::ValidateDocuments { manifest } => {
433 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
434 match commands::validate_documents::run(&manifest, &formatter) {
435 Ok(true) => Ok(()),
436 Ok(false) => {
437 process::exit(2);
438 },
439 Err(e) => Err(e),
440 }
441 },
442
443 Commands::Serve { schema, port } => commands::serve::run(&schema, port).await,
444
445 Commands::Setup { database, dry_run } => {
446 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
447 commands::setup::run(database.as_deref(), dry_run, &formatter).await
448 },
449
450 Commands::Schema { command } => match command {
451 SchemaCommands::Metadata { server, token } => {
452 commands::schema::metadata::run(&server, token.as_deref()).await
453 },
454 },
455
456 Commands::Doctor {
457 config,
458 schema,
459 db_url,
460 against_db,
461 schemas,
462 json: json_flag,
463 } => {
464 let all_passed = commands::doctor::run_with_db_checks(
465 &config,
466 &schema,
467 db_url.as_deref(),
468 against_db.as_deref(),
469 &schemas,
470 json_flag,
471 )
472 .await;
473 if all_passed {
474 Ok(())
475 } else {
476 process::exit(1);
477 }
478 },
479
480 Commands::Perf { command } => match command {
481 PerfCommands::RegressionScan {
482 database,
483 recent_days,
484 baseline_days,
485 min_samples,
486 threshold_pct,
487 min_delta_ms,
488 object_type,
489 fail_on_regression,
490 } => {
491 let args = commands::perf::RegressionScanArgs {
492 database,
493 recent_days,
494 baseline_days,
495 min_samples,
496 threshold_pct,
497 min_delta_ms,
498 object_type,
499 fail_on_regression,
500 json: cli.json,
501 quiet: cli.quiet,
502 };
503 match commands::perf::run_regression_scan(args).await {
504 Ok(true) => Ok(()),
505 Ok(false) => process::exit(1),
506 Err(e) => Err(e),
507 }
508 },
509 PerfCommands::Explore { command } => match command {
510 PerfExploreCommands::Slowest {
511 database,
512 days,
513 object_type,
514 limit,
515 } => {
516 commands::perf::run_explore_slowest(
517 database,
518 days,
519 object_type,
520 limit,
521 cli.json,
522 cli.quiet,
523 )
524 .await
525 },
526 PerfExploreCommands::NullRate {
527 database,
528 days,
529 object_type,
530 } => {
531 commands::perf::run_explore_null_rate(
532 database,
533 days,
534 object_type,
535 cli.json,
536 cli.quiet,
537 )
538 .await
539 },
540 PerfExploreCommands::Summary {
541 database,
542 days,
543 object_type,
544 } => {
545 commands::perf::run_explore_summary(
546 database,
547 days,
548 object_type,
549 cli.json,
550 cli.quiet,
551 )
552 .await
553 },
554 },
555 },
556 };
557
558 if let Err(e) = result {
559 let debug_info = if debug_output {
560 Some(format!("{e:?}"))
561 } else {
562 None
563 };
564 eprintln!("{}", format_cli_error(&e.to_string(), debug_info.as_deref(), json_output, 1));
565 process::exit(1);
566 }
567}
568
569pub(crate) fn format_cli_error(
581 message: &str,
582 debug_info: Option<&str>,
583 json: bool,
584 code: i32,
585) -> String {
586 if json {
587 serde_json::json!({
588 "error": {
589 "message": message,
590 "code": code
591 }
592 })
593 .to_string()
594 } else {
595 let mut out = format!("Error: {message}");
596 if let Some(debug) = debug_info {
597 out.push_str("\n\nDebug info:\n");
598 out.push_str(debug);
599 }
600 out
601 }
602}
603
604fn init_logging(verbose: bool, debug: bool) {
605 let filter = if debug {
606 "fraiseql=debug,fraiseql_core=debug"
607 } else if verbose {
608 "fraiseql=info,fraiseql_core=info"
609 } else {
610 "fraiseql=warn,fraiseql_core=warn"
611 };
612
613 tracing_subscriber::registry()
614 .with(
615 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
616 )
617 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
621 .init();
622}
623
624fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
626 serde_json::to_value(value).unwrap_or_else(|e| {
627 eprintln!("fraiseql: failed to serialize {context}: {e}");
628 std::process::exit(2);
629 })
630}
631
632fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
634 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
635 eprintln!("fraiseql: failed to format {context}: {e}");
636 std::process::exit(2);
637 })
638}
639
640fn handle_introspection_flags() -> Option<i32> {
641 let args: Vec<String> = env::args().collect();
642
643 if args.iter().any(|a| a == "--help-json") {
644 let cmd = Cli::command();
645 let version = env!("CARGO_PKG_VERSION");
646 let help = crate::introspection::extract_cli_help(&cmd, version);
647 let result =
648 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
649 println!("{}", pretty_or_exit(&result, "command result"));
650 return Some(0);
651 }
652
653 if args.iter().any(|a| a == "--list-commands") {
654 let cmd = Cli::command();
655 let commands = crate::introspection::list_commands(&cmd);
656 let result = crate::output::CommandResult::success(
657 "list-commands",
658 serialize_or_exit(&commands, "command list"),
659 );
660 println!("{}", pretty_or_exit(&result, "command result"));
661 return Some(0);
662 }
663
664 let idx = args.iter().position(|a| a == "--show-output-schema")?;
665 let available = crate::output_schemas::list_schema_commands().join(", ");
666
667 let Some(cmd_name) = args.get(idx + 1) else {
668 let result = crate::output::CommandResult::error(
669 "show-output-schema",
670 &format!("Missing command name. Available: {available}"),
671 "MISSING_ARGUMENT",
672 );
673 println!("{}", pretty_or_exit(&result, "command result"));
674 return Some(1);
675 };
676
677 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
678 let result = crate::output::CommandResult::success(
679 "show-output-schema",
680 serialize_or_exit(&schema, "output schema"),
681 );
682 println!("{}", pretty_or_exit(&result, "command result"));
683 return Some(0);
684 }
685
686 let result = crate::output::CommandResult::error(
687 "show-output-schema",
688 &format!("Unknown command: {cmd_name}. Available: {available}"),
689 "UNKNOWN_COMMAND",
690 );
691 println!("{}", pretty_or_exit(&result, "command result"));
692 Some(1)
693}