Skip to main content

spikard_cli/
cli.rs

1//! Spikard CLI – user-facing code generation + testing helpers
2
3use crate::app;
4use crate::codegen::{
5    self, CodegenOutcome, CodegenRequest, CodegenTargetKind, DtoConfig, NodeDtoStyle, PythonDtoStyle, RubyDtoStyle,
6    SchemaKind, TargetLanguage,
7};
8use crate::init::{InitRequest, InitResponse};
9use anyhow::{Context, Result, bail};
10use clap::{Args, Parser, Subcommand, ValueEnum};
11use scythe_core::dialect::SqlDialect;
12use spikard_codegen::sql::DecimalMode;
13use std::ffi::OsString;
14use std::path::PathBuf;
15
16/// Spikard - High-performance HTTP framework with Rust core
17#[derive(Parser, Debug)]
18#[command(author, version, about, long_about = None)]
19struct Cli {
20    #[command(subcommand)]
21    command: Commands,
22}
23
24#[derive(Subcommand, Debug)]
25enum Commands {
26    /// Initialize a new Spikard project
27    Init(InitArgs),
28    /// Start the Spikard MCP server
29    #[cfg(feature = "mcp")]
30    Mcp(McpArgs),
31    /// User-facing code generation entrypoints
32    Generate {
33        #[command(subcommand)]
34        target: GenerateCommand,
35    },
36    /// Test-fixture generation helpers (used by the internal e2e suite)
37    Testing {
38        #[command(subcommand)]
39        target: TestingCommand,
40    },
41    /// Validate an `AsyncAPI` specification
42    ValidateAsyncapi {
43        /// Path to `AsyncAPI` schema file (JSON or YAML)
44        schema: PathBuf,
45    },
46    /// Show information about Spikard
47    Features,
48}
49
50#[derive(Args, Debug)]
51struct InitArgs {
52    /// Name of the project to create
53    name: String,
54
55    /// Target programming language
56    #[arg(long, short = 'l', default_value = "python")]
57    lang: InitLanguage,
58
59    /// Directory where the project will be created (default: current directory)
60    #[arg(long, short = 'd', default_value = ".")]
61    dir: PathBuf,
62}
63
64#[cfg(feature = "mcp")]
65#[derive(Args, Debug)]
66struct McpArgs {
67    /// Transport for the MCP server
68    #[arg(long, default_value = "stdio")]
69    transport: String,
70
71    /// Host to bind when using HTTP transport
72    #[arg(long, default_value = "127.0.0.1")]
73    host: String,
74
75    /// Port to bind when using HTTP transport
76    #[arg(long, default_value_t = 3001)]
77    port: u16,
78}
79
80#[derive(Debug, Clone, Copy, ValueEnum)]
81enum InitLanguage {
82    #[value(name = "python")]
83    Python,
84    #[value(name = "typescript")]
85    TypeScript,
86    #[value(name = "rust")]
87    Rust,
88    #[value(name = "ruby")]
89    Ruby,
90    #[value(name = "php")]
91    Php,
92    #[value(name = "elixir")]
93    Elixir,
94}
95
96impl From<InitLanguage> for TargetLanguage {
97    fn from(lang: InitLanguage) -> Self {
98        match lang {
99            InitLanguage::Python => Self::Python,
100            InitLanguage::TypeScript => Self::TypeScript,
101            InitLanguage::Rust => Self::Rust,
102            InitLanguage::Ruby => Self::Ruby,
103            InitLanguage::Php => Self::Php,
104            InitLanguage::Elixir => Self::Elixir,
105        }
106    }
107}
108
109#[derive(Subcommand, Debug)]
110enum GenerateCommand {
111    /// Generate REST handlers from `OpenAPI` schemas
112    Openapi(OpenapiArgs),
113    /// Generate `AsyncAPI` handler scaffolding (SSE/WebSocket)
114    Asyncapi(AsyncapiHandlerArgs),
115    /// Generate JSON-RPC 2.0 handlers from `OpenRPC` schemas
116    Jsonrpc(JsonrpcArgs),
117    /// Generate GraphQL types, resolvers, or schema
118    Graphql(GraphqlArgs),
119    /// Generate protobuf messages and gRPC services
120    Protobuf(ProtobufArgs),
121    /// Generate PHP DTO classes (Request/Response) for Spikard integration
122    PhpDto(PhpDtoArgs),
123    /// Generate routes + OpenAPI + sidecar from annotated SQL queries (via scythe)
124    Sql(SqlArgs),
125}
126
127#[derive(Args, Debug)]
128struct SqlArgs {
129    /// Directory (or single file) holding `.sql` query files annotated with
130    /// `-- @http <METHOD> <PATH>` etc.
131    queries: PathBuf,
132
133    /// Path(s) to schema DDL — accepts files or directories. Repeat for multiple.
134    #[arg(long = "schema", required = true)]
135    schema: Vec<PathBuf>,
136
137    /// SQL dialect (postgresql, mysql, sqlite, mssql, oracle, redshift, snowflake)
138    #[arg(long, default_value = "postgresql")]
139    dialect: SqlDialectArg,
140
141    /// Output directory (created if missing).
142    #[arg(long, short = 'o', default_value = "generated")]
143    output: PathBuf,
144
145    /// Target languages for sidecar entries. Repeat for multiple.
146    #[arg(long = "lang", num_args = 1..)]
147    lang: Vec<GenerateLanguage>,
148
149    /// How to render the `decimal` neutral type. `string-pattern` (default)
150    /// is lossless; `number` is lossy but ergonomic.
151    #[arg(long, default_value = "string-pattern")]
152    decimal_mode: DecimalModeArg,
153
154    /// Fail on unrecognised neutral types instead of falling back to any-JSON.
155    #[arg(long, default_value_t = false)]
156    strict: bool,
157
158    /// Skip emitting the OpenAPI 3.1 spec alongside routes + sidecar.
159    #[arg(long = "no-openapi", default_value_t = false)]
160    no_openapi: bool,
161
162    /// API title for the OpenAPI spec.
163    #[arg(long, default_value = "Generated API")]
164    api_title: String,
165
166    /// API version for the OpenAPI spec.
167    #[arg(long, default_value = "0.1.0")]
168    api_version: String,
169}
170
171#[derive(Debug, Clone, Copy, ValueEnum)]
172enum SqlDialectArg {
173    #[value(name = "postgresql", alias = "postgres", alias = "redshift", alias = "cockroachdb")]
174    PostgreSQL,
175    #[value(name = "mysql", alias = "mariadb")]
176    MySQL,
177    #[value(name = "sqlite")]
178    SQLite,
179    #[value(name = "mssql", alias = "sqlserver")]
180    MsSql,
181    #[value(name = "oracle")]
182    Oracle,
183    #[value(name = "snowflake")]
184    Snowflake,
185}
186
187impl From<SqlDialectArg> for SqlDialect {
188    fn from(d: SqlDialectArg) -> Self {
189        match d {
190            SqlDialectArg::PostgreSQL => SqlDialect::PostgreSQL,
191            SqlDialectArg::MySQL => SqlDialect::MySQL,
192            SqlDialectArg::SQLite => SqlDialect::SQLite,
193            SqlDialectArg::MsSql => SqlDialect::MsSql,
194            SqlDialectArg::Oracle => SqlDialect::Oracle,
195            SqlDialectArg::Snowflake => SqlDialect::Snowflake,
196        }
197    }
198}
199
200#[derive(Debug, Clone, Copy, ValueEnum)]
201enum DecimalModeArg {
202    #[value(name = "string-pattern")]
203    StringPattern,
204    #[value(name = "number")]
205    Number,
206}
207
208impl From<DecimalModeArg> for DecimalMode {
209    fn from(m: DecimalModeArg) -> Self {
210        match m {
211            DecimalModeArg::StringPattern => DecimalMode::StringPattern,
212            DecimalModeArg::Number => DecimalMode::Number,
213        }
214    }
215}
216
217#[derive(Args, Debug)]
218struct OpenapiArgs {
219    /// Path to `OpenAPI` schema file (JSON or YAML)
220    schema: PathBuf,
221
222    /// Target language for code generation
223    #[arg(long, short = 'l', default_value = "python")]
224    lang: GenerateLanguage,
225
226    /// Output file path (prints to stdout if not specified)
227    #[arg(long, short = 'o')]
228    output: Option<PathBuf>,
229
230    /// DTO implementation for the selected language (defaults per language)
231    #[arg(long = "dto", value_enum)]
232    dto: Option<DtoArg>,
233}
234
235#[derive(Args, Debug)]
236struct AsyncapiHandlerArgs {
237    /// Path to `AsyncAPI` schema file (JSON or YAML)
238    schema: PathBuf,
239
240    /// Target language for handler scaffolding
241    #[arg(long, short = 'l')]
242    lang: GenerateLanguage,
243
244    /// Output file path
245    #[arg(long, short = 'o')]
246    output: PathBuf,
247
248    /// DTO implementation for the selected language (defaults per language)
249    #[arg(long = "dto", value_enum)]
250    dto: Option<DtoArg>,
251}
252
253#[derive(Args, Debug)]
254struct JsonrpcArgs {
255    /// Path to `OpenRPC` schema file (JSON or YAML)
256    schema: PathBuf,
257
258    /// Target language for handler scaffolding
259    #[arg(long, short = 'l', default_value = "python")]
260    lang: GenerateLanguage,
261
262    /// Output file path (prints to stdout if not specified)
263    #[arg(long, short = 'o')]
264    output: Option<PathBuf>,
265}
266
267#[derive(Args, Debug)]
268struct GraphqlArgs {
269    /// Path to GraphQL schema file (.graphql, .gql, or .json for introspection)
270    schema: PathBuf,
271
272    /// Target language (python, typescript, rust, ruby, php)
273    #[arg(long, short = 'l', default_value = "python")]
274    lang: GenerateLanguage,
275
276    /// Output file path (prints to stdout if not specified)
277    #[arg(long, short = 'o')]
278    output: Option<PathBuf>,
279
280    /// Target specific features (all, types, resolvers, schema)
281    #[arg(long, default_value = "all")]
282    target: String,
283}
284
285#[derive(Args, Debug)]
286struct ProtobufArgs {
287    /// Path to .proto schema file
288    schema: PathBuf,
289
290    /// Target language (python, typescript, ruby, php)
291    #[arg(long, short = 'l', default_value = "python")]
292    lang: GenerateLanguage,
293
294    /// Output file path
295    #[arg(long, short = 'o')]
296    output: PathBuf,
297
298    /// Target: all, messages, or services
299    #[arg(long, default_value = "all")]
300    target: String,
301
302    /// Additional import directories used to resolve imported .proto files
303    #[arg(long = "include")]
304    include: Vec<PathBuf>,
305}
306
307#[derive(Subcommand, Debug)]
308enum TestingCommand {
309    /// AsyncAPI-specific fixture + harness generators
310    Asyncapi {
311        #[command(subcommand)]
312        target: AsyncapiTestingTarget,
313    },
314}
315
316#[derive(Subcommand, Debug)]
317enum AsyncapiTestingTarget {
318    /// Generate test fixtures from message schemas
319    Fixtures(AsyncFixtureArgs),
320    /// Generate test application for a specific language
321    TestApp(AsyncTestAppArgs),
322    /// Generate everything (fixtures + test apps for all languages)
323    All(AsyncAllArgs),
324}
325
326#[derive(Args, Debug)]
327struct AsyncFixtureArgs {
328    /// Path to `AsyncAPI` schema file (JSON or YAML)
329    schema: PathBuf,
330    /// Output directory for fixtures (default: `testing_data`/)
331    #[arg(long, short = 'o', default_value = "testing_data")]
332    output: PathBuf,
333}
334
335#[derive(Args, Debug)]
336struct AsyncTestAppArgs {
337    /// Path to `AsyncAPI` schema file (JSON or YAML)
338    schema: PathBuf,
339    /// Target language
340    #[arg(long, short = 'l')]
341    lang: GenerateLanguage,
342    /// Output file path
343    #[arg(long, short = 'o')]
344    output: PathBuf,
345}
346
347#[derive(Args, Debug)]
348struct AsyncAllArgs {
349    /// Path to `AsyncAPI` schema file (JSON or YAML)
350    schema: PathBuf,
351    /// Output directory (default: current directory)
352    #[arg(long, short = 'o', default_value = ".")]
353    output: PathBuf,
354}
355
356#[derive(Args, Debug)]
357struct PhpDtoArgs {
358    /// Output directory for generated DTO classes (default: src/Generated)
359    #[arg(long, short = 'o', default_value = "src/Generated")]
360    output: PathBuf,
361}
362
363#[derive(Debug, Clone, Copy, ValueEnum)]
364enum GenerateLanguage {
365    #[value(name = "python")]
366    Python,
367    #[value(name = "typescript")]
368    TypeScript,
369    #[value(name = "rust")]
370    Rust,
371    #[value(name = "ruby")]
372    Ruby,
373    #[value(name = "php")]
374    Php,
375    #[value(name = "elixir")]
376    Elixir,
377}
378
379impl From<GenerateLanguage> for codegen::TargetLanguage {
380    fn from(lang: GenerateLanguage) -> Self {
381        match lang {
382            GenerateLanguage::Python => Self::Python,
383            GenerateLanguage::TypeScript => Self::TypeScript,
384            GenerateLanguage::Rust => Self::Rust,
385            GenerateLanguage::Ruby => Self::Ruby,
386            GenerateLanguage::Php => Self::Php,
387            GenerateLanguage::Elixir => Self::Elixir,
388        }
389    }
390}
391
392fn apply_dto_selection(config: &mut DtoConfig, lang: GenerateLanguage, dto: DtoArg) -> Result<()> {
393    match lang {
394        GenerateLanguage::Python => match dto {
395            DtoArg::Dataclass => config.python = PythonDtoStyle::Dataclass,
396            DtoArg::Msgspec => config.python = PythonDtoStyle::Msgspec,
397            _ => bail!("DTO '{dto:?}' is not supported for Python"),
398        },
399        GenerateLanguage::TypeScript => match dto {
400            DtoArg::Zod => config.node = NodeDtoStyle::Zod,
401            _ => bail!("DTO '{dto:?}' is not supported for TypeScript"),
402        },
403        GenerateLanguage::Ruby => match dto {
404            DtoArg::DrySchema => config.ruby = RubyDtoStyle::DrySchema,
405            _ => bail!("DTO '{dto:?}' is not supported for Ruby"),
406        },
407        GenerateLanguage::Rust => match dto {
408            DtoArg::Serde => config.rust = codegen::RustDtoStyle::SerdeStruct,
409            _ => bail!("DTO '{dto:?}' is not supported for Rust"),
410        },
411        GenerateLanguage::Php => match dto {
412            DtoArg::ReadonlyClass => config.php = codegen::PhpDtoStyle::ReadonlyClass,
413            _ => bail!("DTO '{dto:?}' is not supported for PHP"),
414        },
415        GenerateLanguage::Elixir => bail!("DTO '{dto:?}' is not supported for Elixir"),
416    }
417    Ok(())
418}
419
420fn default_jsonrpc_output(lang: GenerateLanguage) -> PathBuf {
421    let ext = match lang {
422        GenerateLanguage::Python => "py",
423        GenerateLanguage::TypeScript => "ts",
424        GenerateLanguage::Rust => "rs",
425        GenerateLanguage::Ruby => "rb",
426        GenerateLanguage::Php => "php",
427        GenerateLanguage::Elixir => "ex",
428    };
429
430    PathBuf::from(format!("handlers.{ext}"))
431}
432
433fn default_graphql_output(lang: GenerateLanguage) -> PathBuf {
434    let ext = match lang {
435        GenerateLanguage::Python => "py",
436        GenerateLanguage::TypeScript => "ts",
437        GenerateLanguage::Rust => "rs",
438        GenerateLanguage::Ruby => "rb",
439        GenerateLanguage::Php => "php",
440        GenerateLanguage::Elixir => "ex",
441    };
442
443    PathBuf::from(format!("generated.{ext}"))
444}
445
446#[derive(Debug, Clone, Copy, ValueEnum)]
447enum DtoArg {
448    Dataclass,
449    Msgspec,
450    Zod,
451    DrySchema,
452    Serde,
453    ReadonlyClass,
454}
455
456pub fn run_from_env() -> Result<()> {
457    run(Cli::parse())
458}
459
460pub fn run_from<I, T>(args: I) -> Result<()>
461where
462    I: IntoIterator<Item = T>,
463    T: Into<OsString> + Clone,
464{
465    run(Cli::try_parse_from(args)?)
466}
467
468fn run(cli: Cli) -> Result<()> {
469    match cli.command {
470        Commands::Init(args) => {
471            println!("Creating new Spikard project...");
472            println!("  Project name: {}", args.name);
473            println!("  Language: {:?}", args.lang);
474            println!("  Directory: {}", args.dir.display());
475            println!();
476
477            let request = InitRequest {
478                project_name: args.name.clone(),
479                language: args.lang.into(),
480                project_dir: args.dir.join(&args.name),
481                schema_path: None,
482            };
483
484            match app::init_project(request) {
485                Ok(response) => {
486                    print_init_response(response);
487                }
488                Err(e) => {
489                    eprintln!("✗ Failed to create project: {e}");
490                    return Err(e);
491                }
492            }
493        }
494        #[cfg(feature = "mcp")]
495        Commands::Mcp(args) => {
496            let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime for MCP server")?;
497            match args.transport.to_ascii_lowercase().as_str() {
498                "stdio" => runtime
499                    .block_on(crate::mcp::start_mcp_server())
500                    .map_err(|error| anyhow::anyhow!(error.to_string()))
501                    .context("Failed to start MCP server over stdio")?,
502                "http" => {
503                    #[cfg(not(feature = "mcp-http"))]
504                    {
505                        bail!("HTTP transport requires the 'mcp-http' feature");
506                    }
507
508                    #[cfg(feature = "mcp-http")]
509                    runtime
510                        .block_on(crate::mcp::start_mcp_server_http(&args.host, args.port))
511                        .map_err(|error| anyhow::anyhow!(error.to_string()))
512                        .with_context(|| {
513                            format!("Failed to start MCP server over http://{}:{}", args.host, args.port)
514                        })?;
515                }
516                other => bail!("Unknown MCP transport '{other}'. Use 'stdio' or 'http'"),
517            }
518        }
519        Commands::Generate { target } => match target {
520            GenerateCommand::PhpDto(args) => {
521                println!("Generating PHP DTO classes for Spikard...");
522                println!("  Output directory: {}", args.output.display());
523                let assets = app::generate_php_dto(&args.output)?;
524                print_codegen_outcome(CodegenOutcome::Files(assets));
525            }
526            GenerateCommand::Sql(args) => {
527                if args.lang.is_empty() {
528                    bail!("At least one --lang is required for `generate sql` (e.g. --lang python --lang typescript)");
529                }
530                println!("Generating handlers from annotated SQL...");
531                println!("  Queries: {}", args.queries.display());
532                println!("  Output:  {}", args.output.display());
533                let languages: Vec<TargetLanguage> = args.lang.iter().map(|l| (*l).into()).collect();
534                let request = CodegenRequest {
535                    schema_path: args.queries.clone(),
536                    schema_kind: SchemaKind::Sql,
537                    target: CodegenTargetKind::SqlHandlers {
538                        schema_paths: args.schema.clone(),
539                        output: args.output,
540                        dialect: args.dialect.into(),
541                        languages,
542                        decimal_mode: args.decimal_mode.into(),
543                        strict: args.strict,
544                        emit_openapi: !args.no_openapi,
545                        api_title: args.api_title,
546                        api_version: args.api_version,
547                    },
548                    dto: None,
549                };
550                let outcome = app::execute_codegen(request).context("Failed to generate handlers from SQL")?;
551                print_codegen_outcome(outcome);
552            }
553            GenerateCommand::Openapi(args) => {
554                let mut dto_config = DtoConfig::default();
555                if let Some(arg) = args.dto {
556                    apply_dto_selection(&mut dto_config, args.lang, arg)?;
557                }
558                let request = CodegenRequest {
559                    schema_path: args.schema.clone(),
560                    schema_kind: SchemaKind::OpenApi,
561                    target: CodegenTargetKind::Server {
562                        language: args.lang.into(),
563                        output: args.output,
564                    },
565                    dto: Some(dto_config),
566                };
567
568                let outcome = app::execute_codegen(request).context("Failed to generate code from OpenAPI schema")?;
569                print_codegen_outcome(outcome);
570            }
571            GenerateCommand::Asyncapi(args) => {
572                println!("Generating handler scaffolding from AsyncAPI schema...");
573                println!("  Input: {}", args.schema.display());
574                println!("  Language: {:?}", args.lang);
575                println!("  Output: {}", args.output.display());
576                let mut dto_config = DtoConfig::default();
577                if let Some(arg) = args.dto {
578                    apply_dto_selection(&mut dto_config, args.lang, arg)?;
579                }
580                let request = CodegenRequest {
581                    schema_path: args.schema.clone(),
582                    schema_kind: SchemaKind::AsyncApi,
583                    target: CodegenTargetKind::AsyncHandlers {
584                        language: args.lang.into(),
585                        output: args.output,
586                    },
587                    dto: Some(dto_config),
588                };
589                print_codegen_outcome(app::execute_codegen(request)?);
590            }
591            GenerateCommand::Jsonrpc(args) => {
592                println!("Generating JSON-RPC 2.0 handlers from OpenRPC schema...");
593                println!("  Input: {}", args.schema.display());
594                println!("  Language: {:?}", args.lang);
595                if let Some(ref path) = args.output {
596                    println!("  Output: {}", path.display());
597                }
598                let request = CodegenRequest {
599                    schema_path: args.schema.clone(),
600                    schema_kind: SchemaKind::OpenRpc,
601                    target: CodegenTargetKind::JsonRpcHandlers {
602                        language: args.lang.into(),
603                        output: args.output.unwrap_or_else(|| default_jsonrpc_output(args.lang)),
604                    },
605                    dto: None,
606                };
607
608                let outcome = app::execute_codegen(request).context("Failed to generate code from OpenRPC schema")?;
609                print_codegen_outcome(outcome);
610            }
611            GenerateCommand::Graphql(args) => {
612                println!("Generating GraphQL code from schema...");
613                println!("  Input: {}", args.schema.display());
614                println!("  Language: {:?}", args.lang);
615                println!("  Target: {}", args.target);
616                if let Some(ref path) = args.output {
617                    println!("  Output: {}", path.display());
618                }
619                let output_path = args.output.clone().unwrap_or_else(|| default_graphql_output(args.lang));
620
621                let request = CodegenRequest {
622                    schema_path: args.schema.clone(),
623                    schema_kind: SchemaKind::GraphQL,
624                    target: CodegenTargetKind::GraphQL {
625                        language: args.lang.into(),
626                        output: output_path,
627                        target: args.target,
628                    },
629                    dto: None,
630                };
631
632                let outcome = app::execute_codegen(request).context("Failed to generate code from GraphQL schema")?;
633                print_codegen_outcome(outcome);
634            }
635            GenerateCommand::Protobuf(args) => {
636                println!("Generating protobuf code from schema...");
637                println!("  Input: {}", args.schema.display());
638                println!("  Language: {:?}", args.lang);
639                println!("  Target: {}", args.target);
640                println!("  Output: {}", args.output.display());
641
642                let request = CodegenRequest {
643                    schema_path: args.schema.clone(),
644                    schema_kind: SchemaKind::Protobuf,
645                    target: CodegenTargetKind::Protobuf {
646                        language: args.lang.into(),
647                        output: args.output.clone(),
648                        target: args.target,
649                        include_paths: args.include,
650                    },
651                    dto: None,
652                };
653
654                let outcome = app::execute_codegen(request).context("Failed to generate protobuf code")?;
655                print_codegen_outcome(outcome);
656            }
657        },
658        Commands::Testing { target } => match target {
659            TestingCommand::Asyncapi { target } => match target {
660                AsyncapiTestingTarget::Fixtures(args) => {
661                    println!("Generating test fixtures from AsyncAPI schema...");
662                    println!("  Input: {}", args.schema.display());
663                    println!("  Output: {}", args.output.display());
664                    let request = CodegenRequest {
665                        schema_path: args.schema.clone(),
666                        schema_kind: SchemaKind::AsyncApi,
667                        target: CodegenTargetKind::AsyncFixtures { output: args.output },
668                        dto: None,
669                    };
670                    let files = match app::execute_codegen_unvalidated(request)? {
671                        CodegenOutcome::Files(files) => files,
672                        CodegenOutcome::InMemory(_) => unreachable!("Fixtures always write files"),
673                    };
674                    println!("\n✓ Generated {} fixture files", files.len());
675                }
676                AsyncapiTestingTarget::TestApp(args) => {
677                    println!("Generating test application from AsyncAPI schema...");
678                    println!("  Input: {}", args.schema.display());
679                    println!("  Language: {:?}", args.lang);
680                    println!("  Output: {}", args.output.display());
681                    let request = CodegenRequest {
682                        schema_path: args.schema.clone(),
683                        schema_kind: SchemaKind::AsyncApi,
684                        target: CodegenTargetKind::AsyncTestApp {
685                            language: args.lang.into(),
686                            output: args.output,
687                        },
688                        dto: None,
689                    };
690                    print_codegen_outcome(app::execute_codegen_unvalidated(request)?);
691                }
692                AsyncapiTestingTarget::All(args) => {
693                    println!("Generating all assets from AsyncAPI schema...");
694                    println!("  Input: {}", args.schema.display());
695                    println!("  Output directory: {}", args.output.display());
696                    let request = CodegenRequest {
697                        schema_path: args.schema.clone(),
698                        schema_kind: SchemaKind::AsyncApi,
699                        target: CodegenTargetKind::AsyncAll { output: args.output },
700                        dto: None,
701                    };
702                    let files = match app::execute_codegen_unvalidated(request)? {
703                        CodegenOutcome::Files(files) => files,
704                        CodegenOutcome::InMemory(_) => unreachable!("AsyncAPI bundle writes files"),
705                    };
706                    println!("\n✓ Generated {} assets:", files.len());
707                    for asset in files {
708                        println!("  - {} -> {}", asset.description, asset.path.display());
709                    }
710                }
711            },
712        },
713        Commands::Features => {
714            print_feature_summary(app::feature_summary());
715        }
716        Commands::ValidateAsyncapi { schema } => {
717            print_asyncapi_validation(app::validate_asyncapi_schema(&schema)?);
718        }
719    }
720
721    Ok(())
722}
723
724fn print_init_response(response: InitResponse) {
725    println!("✓ Project created successfully!");
726    println!();
727    println!("Created {} files:", response.files_created.len());
728    for file in response.files_created {
729        println!("  - {}", file.display());
730    }
731    println!();
732    println!("Next steps:");
733    for (i, step) in response.next_steps.iter().enumerate() {
734        println!("  {}. {}", i + 1, step);
735    }
736}
737
738fn print_codegen_outcome(outcome: CodegenOutcome) {
739    match outcome {
740        CodegenOutcome::InMemory(code) => println!("{code}"),
741        CodegenOutcome::Files(files) => {
742            for asset in files {
743                println!("✓ Generated {} at {}", asset.description, asset.path.display());
744            }
745        }
746    }
747}
748
749fn print_feature_summary(summary: app::FeatureSummary) {
750    println!("Spikard - High-performance HTTP framework\n");
751    println!("Rust Core: {}", if summary.rust_core { "✓" } else { "✗" });
752    println!("\nLanguage Bindings:");
753    for binding in &summary.language_bindings {
754        println!("  {}: {}", binding.name, binding.install_hint);
755    }
756    println!("\nUsage:");
757    for binding in &summary.language_bindings {
758        println!("  {}: {}", binding.name, binding.usage_hint);
759    }
760    println!("\nDocumentation: {}", summary.documentation_url);
761}
762
763fn print_asyncapi_validation(summary: app::AsyncApiValidationSummary) {
764    println!("✓ AsyncAPI schema is valid");
765    println!("  Spec Version: {}", summary.spec_version);
766    println!("  Title: {}", summary.title);
767    println!("  API Version: {}", summary.api_version);
768    println!("  Primary Protocol: {}", summary.primary_protocol);
769    println!("  Channels: {}", summary.channel_count);
770    println!("\nSchema validated successfully!");
771}