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 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::Validate {
213 command,
214 input,
215 check_cycles,
216 check_unused,
217 strict,
218 types,
219 } => match command {
220 Some(ValidateCommands::Facts { schema, database }) => {
221 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
222 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
223 .await
224 },
225 None => match input {
226 Some(input) => {
227 let opts = commands::validate::ValidateOptions {
228 check_cycles,
229 check_unused,
230 strict,
231 filter_types: types,
232 };
233 match commands::validate::run_with_options(&input, opts) {
234 Ok(result) => {
235 println!(
236 "{}",
237 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
238 );
239 if result.status == "validation-failed" {
240 Err(anyhow::anyhow!("Validation failed"))
241 } else {
242 Ok(())
243 }
244 },
245 Err(e) => Err(e),
246 }
247 },
248 None => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
249 },
250 },
251
252 Commands::Introspect { command } => match command {
253 IntrospectCommands::Facts { database, format } => {
254 match commands::introspect_facts::OutputFormat::parse(&format) {
255 Ok(fmt) => {
256 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
257 commands::introspect_facts::run(&database, fmt, &formatter).await
258 },
259 Err(e) => Err(anyhow::anyhow!(e)),
260 }
261 },
262 },
263
264 Commands::Generate {
265 input,
266 language,
267 output,
268 } => match commands::init::Language::from_str(&language) {
269 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
270 Err(e) => Err(anyhow::anyhow!(e)),
271 },
272
273 Commands::GenerateClient { language } => match language {
274 GenerateClientCommands::Typescript { schema, out, force } => {
275 commands::generate_client::run(
276 commands::generate_client::ClientLanguage::TypeScript,
277 schema.as_deref(),
278 &out,
279 force,
280 )
281 },
282 },
283
284 Commands::Init {
285 project_name,
286 language,
287 database,
288 size,
289 no_git,
290 } => {
291 match (
292 commands::init::Language::from_str(&language),
293 commands::init::Database::from_str(&database),
294 commands::init::ProjectSize::from_str(&size),
295 ) {
296 (Ok(lang), Ok(db), Ok(sz)) => {
297 let config = commands::init::InitConfig {
298 project_name,
299 language: lang,
300 database: db,
301 size: sz,
302 no_git,
303 };
304 commands::init::run(&config)
305 },
306 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
307 }
308 },
309
310 Commands::Migrate { command } => {
311 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
312 match command {
313 MigrateCommands::Up { database, dir } => {
314 let db_url = commands::migrate::resolve_database_url(database.as_deref());
315 match db_url {
316 Ok(url) => {
317 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
318 let action = commands::migrate::MigrateAction::Up {
319 database_url: url,
320 dir: mig_dir,
321 };
322 commands::migrate::run(&action, &formatter)
323 },
324 Err(e) => Err(e),
325 }
326 },
327 MigrateCommands::Down {
328 database,
329 dir,
330 steps,
331 } => {
332 let db_url = commands::migrate::resolve_database_url(database.as_deref());
333 match db_url {
334 Ok(url) => {
335 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
336 let action = commands::migrate::MigrateAction::Down {
337 database_url: url,
338 dir: mig_dir,
339 steps,
340 };
341 commands::migrate::run(&action, &formatter)
342 },
343 Err(e) => Err(e),
344 }
345 },
346 MigrateCommands::Status { database, dir } => {
347 let db_url = commands::migrate::resolve_database_url(database.as_deref());
348 match db_url {
349 Ok(url) => {
350 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
351 let action = commands::migrate::MigrateAction::Status {
352 database_url: url,
353 dir: mig_dir,
354 };
355 commands::migrate::run(&action, &formatter)
356 },
357 Err(e) => Err(e),
358 }
359 },
360 MigrateCommands::Create { name, dir } => {
361 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
362 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
363 commands::migrate::run(&action, &formatter)
364 },
365 MigrateCommands::Generate { name, dir } => {
366 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
367 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
368 commands::migrate::run(&action, &formatter)
369 },
370 MigrateCommands::Validate { dir } => {
371 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
372 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
373 commands::migrate::run(&action, &formatter)
374 },
375 MigrateCommands::Preflight { dir } => {
376 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
377 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
378 commands::migrate::run(&action, &formatter)
379 },
380 }
381 },
382
383 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
384 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
385 Err(e) => Err(anyhow::anyhow!(e)),
386 },
387
388 #[cfg(feature = "run-server")]
389 Commands::Run {
390 input,
391 database,
392 port,
393 bind,
394 watch,
395 introspection,
396 } => {
397 Box::pin(commands::run::run(
401 input.as_deref(),
402 database,
403 port,
404 bind,
405 watch,
406 introspection,
407 ))
408 .await
409 },
410
411 Commands::ValidateDocuments { manifest } => {
412 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
413 match commands::validate_documents::run(&manifest, &formatter) {
414 Ok(true) => Ok(()),
415 Ok(false) => {
416 process::exit(2);
417 },
418 Err(e) => Err(e),
419 }
420 },
421
422 Commands::Serve { schema, port } => commands::serve::run(&schema, port).await,
423
424 Commands::Setup { database, dry_run } => {
425 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
426 commands::setup::run(database.as_deref(), dry_run, &formatter).await
427 },
428
429 Commands::Schema { command } => match command {
430 SchemaCommands::Metadata { server, token } => {
431 commands::schema::metadata::run(&server, token.as_deref()).await
432 },
433 },
434
435 Commands::Doctor {
436 config,
437 schema,
438 db_url,
439 json: json_flag,
440 } => {
441 let all_passed = commands::doctor::run(&config, &schema, db_url.as_deref(), json_flag);
442 if all_passed {
443 Ok(())
444 } else {
445 process::exit(1);
446 }
447 },
448 };
449
450 if let Err(e) = result {
451 let debug_info = if debug_output {
452 Some(format!("{e:?}"))
453 } else {
454 None
455 };
456 eprintln!("{}", format_cli_error(&e.to_string(), debug_info.as_deref(), json_output, 1));
457 process::exit(1);
458 }
459}
460
461pub(crate) fn format_cli_error(
473 message: &str,
474 debug_info: Option<&str>,
475 json: bool,
476 code: i32,
477) -> String {
478 if json {
479 serde_json::json!({
480 "error": {
481 "message": message,
482 "code": code
483 }
484 })
485 .to_string()
486 } else {
487 let mut out = format!("Error: {message}");
488 if let Some(debug) = debug_info {
489 out.push_str("\n\nDebug info:\n");
490 out.push_str(debug);
491 }
492 out
493 }
494}
495
496fn init_logging(verbose: bool, debug: bool) {
497 let filter = if debug {
498 "fraiseql=debug,fraiseql_core=debug"
499 } else if verbose {
500 "fraiseql=info,fraiseql_core=info"
501 } else {
502 "fraiseql=warn,fraiseql_core=warn"
503 };
504
505 tracing_subscriber::registry()
506 .with(
507 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
508 )
509 .with(tracing_subscriber::fmt::layer())
510 .init();
511}
512
513fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
515 serde_json::to_value(value).unwrap_or_else(|e| {
516 eprintln!("fraiseql: failed to serialize {context}: {e}");
517 std::process::exit(2);
518 })
519}
520
521fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
523 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
524 eprintln!("fraiseql: failed to format {context}: {e}");
525 std::process::exit(2);
526 })
527}
528
529fn handle_introspection_flags() -> Option<i32> {
530 let args: Vec<String> = env::args().collect();
531
532 if args.iter().any(|a| a == "--help-json") {
533 let cmd = Cli::command();
534 let version = env!("CARGO_PKG_VERSION");
535 let help = crate::introspection::extract_cli_help(&cmd, version);
536 let result =
537 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
538 println!("{}", pretty_or_exit(&result, "command result"));
539 return Some(0);
540 }
541
542 if args.iter().any(|a| a == "--list-commands") {
543 let cmd = Cli::command();
544 let commands = crate::introspection::list_commands(&cmd);
545 let result = crate::output::CommandResult::success(
546 "list-commands",
547 serialize_or_exit(&commands, "command list"),
548 );
549 println!("{}", pretty_or_exit(&result, "command result"));
550 return Some(0);
551 }
552
553 let idx = args.iter().position(|a| a == "--show-output-schema")?;
554 let available = crate::output_schemas::list_schema_commands().join(", ");
555
556 let Some(cmd_name) = args.get(idx + 1) else {
557 let result = crate::output::CommandResult::error(
558 "show-output-schema",
559 &format!("Missing command name. Available: {available}"),
560 "MISSING_ARGUMENT",
561 );
562 println!("{}", pretty_or_exit(&result, "command result"));
563 return Some(1);
564 };
565
566 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
567 let result = crate::output::CommandResult::success(
568 "show-output-schema",
569 serialize_or_exit(&schema, "output schema"),
570 );
571 println!("{}", pretty_or_exit(&result, "command result"));
572 return Some(0);
573 }
574
575 let result = crate::output::CommandResult::error(
576 "show-output-schema",
577 &format!("Unknown command: {cmd_name}. Available: {available}"),
578 "UNKNOWN_COMMAND",
579 );
580 println!("{}", pretty_or_exit(&result, "command result"));
581 Some(1)
582}