serverust_cli/lib.rs
1//! CLI do framework **serverust**, com paridade conceitual ao `@nestjs/cli`.
2//!
3//! O binário `serverust` provê comandos para scaffolding e workflow de
4//! desenvolvimento/deploy:
5//!
6//! ```text
7//! serverust new <name> # cria um projeto novo
8//! serverust generate <kind> <name> # scaffolding (resource, module, ...)
9//! serverust dev # cargo watch -x run
10//! serverust build [--release] # cargo build
11//! serverust deploy lambda [--arch arm64|x86_64]
12//! serverust info # versões e features
13//! serverust openapi --out openapi.json # exporta spec sem subir servidor
14//! ```
15//!
16//! Este crate expõe também a lib (`serverust_cli`) com módulos
17//! [`cli`] (definições clap), [`commands`] (construção testável de
18//! `std::process::Command`), [`scaffold`] (IO em base dir parametrizada) e
19//! [`templates`] (strings de scaffolding). A separação permite testar parse +
20//! geração de arquivos em tempdir sem spawn de processos reais.
21
22pub mod cli;
23pub mod commands;
24pub mod scaffold;
25pub mod templates;
26
27use anyhow::Result;
28
29use crate::cli::{Cli, Command, DeployTarget};
30
31/// Executa um comando da CLI já parseado.
32///
33/// Operações de IO (criação de arquivos, spawn de processos) são executadas
34/// aqui; a separação em módulos mantém a lógica testável sem efeitos colaterais.
35pub fn run(cli: Cli) -> Result<()> {
36 match cli.command {
37 Command::New { name } => {
38 let cwd = std::env::current_dir()?;
39 scaffold::new_project(&cwd, &name)?;
40 println!("✓ project created at {}/{}", cwd.display(), name);
41 Ok(())
42 }
43 Command::Generate { kind, name } => {
44 let cwd = std::env::current_dir()?;
45 scaffold::generate(&cwd, kind, &name)?;
46 println!("✓ {kind:?} '{name}' generated");
47 Ok(())
48 }
49 Command::Dev => {
50 require_cargo_subcommand("watch", "cargo install cargo-watch")?;
51 // Aviso amigável: o primeiro build puxa ~150-200 crates (axum, tokio,
52 // hyper, utoipa...). Subsequentes são incrementais e rápidos.
53 if !std::path::Path::new("target").exists() {
54 eprintln!(
55 "🦀 primeira compilação puxa muitas deps e pode levar 2-3min;\n builds subsequentes são incrementais e bem mais rápidos.\n"
56 );
57 }
58 spawn_status(commands::dev_cargo_command(), "dev")
59 }
60 Command::Build { release } => spawn_status(commands::build_cargo_command(release), "build"),
61 Command::Deploy { target } => match target {
62 DeployTarget::Lambda { arch } => {
63 require_cargo_subcommand(
64 "lambda",
65 "cargo install cargo-lambda # ou https://www.cargo-lambda.info/guide/installation.html",
66 )?;
67 spawn_status(commands::deploy_lambda_cargo_command(arch), "deploy lambda")
68 }
69 },
70 Command::Info => {
71 println!("{}", commands::info_text());
72 Ok(())
73 }
74 Command::Openapi { out } => spawn_status(commands::openapi_export_command(&out), "openapi"),
75 }
76}
77
78/// Confirma que `cargo-<subcommand>` está disponível no PATH antes de chamar.
79///
80/// Em vez de deixar o cargo cuspir o erro padrão (`error: no such command: ...`),
81/// emitimos uma mensagem com o comando exato de instalação. Reduz fricção para
82/// quem está descobrindo o framework e ainda não conhece o ecossistema.
83fn require_cargo_subcommand(subcommand: &str, install_hint: &str) -> Result<()> {
84 let binary = format!("cargo-{subcommand}");
85 let installed = std::process::Command::new(&binary)
86 .arg("--version")
87 .output()
88 .map(|o| o.status.success())
89 .unwrap_or(false);
90 if installed {
91 return Ok(());
92 }
93 anyhow::bail!(
94 "`cargo {subcommand}` não está disponível (necessário para este comando).\n\
95 Instale com:\n {install_hint}"
96 );
97}
98
99fn spawn_status(mut cmd: std::process::Command, label: &str) -> Result<()> {
100 let status = cmd
101 .status()
102 .map_err(|e| anyhow::anyhow!("failed to spawn {label}: {e}"))?;
103 if !status.success() {
104 anyhow::bail!("{label} failed with status {status}");
105 }
106 Ok(())
107}