Skip to main content

doido_generators/
cli.rs

1use crate::commands::{self, jobs::JobsCommand, new::run_new};
2use crate::new_options::{CacheBackend, DatabaseBackend, JobsBackend, NewOptions};
3use crate::DOIDO_VERSION;
4use clap::{Parser, Subcommand};
5use doido_controller::axum;
6
7#[derive(Parser)]
8#[command(name = "doido", version = DOIDO_VERSION, about = "Doido framework CLI")]
9struct Cli {
10    #[command(subcommand)]
11    command: Commands,
12}
13
14#[derive(Subcommand)]
15// Parsed once at startup, so the variant-size disparity from clap's embedded
16// subcommands is irrelevant; boxing derived subcommand fields is fragile.
17#[allow(clippy::large_enum_variant)]
18enum Commands {
19    /// Start the web server
20    Server {
21        /// Port to bind (overrides `server.port` in config/<env>.yml)
22        #[arg(long)]
23        port: Option<u16>,
24        /// Environment for this run: development | test | production (sets DOIDO_ENV)
25        #[arg(long)]
26        env: Option<String>,
27    },
28    /// Print routes
29    Routes,
30    /// Start interactive console
31    Console,
32    /// Database commands (create, SeaORM migrations and entity codegen)
33    Db {
34        /// Show debug messages
35        #[arg(short, long, global = true)]
36        verbose: bool,
37        #[command(subcommand)]
38        command: commands::db::DbCommand,
39    },
40    /// Background job commands
41    Jobs {
42        #[command(subcommand)]
43        action: JobsCommand,
44    },
45    /// Start background worker
46    Worker {
47        /// Drain the jobs currently ready, then exit (instead of running until Ctrl-C).
48        #[arg(long)]
49        once: bool,
50    },
51    /// Manage credentials
52    Credentials {
53        #[command(subcommand)]
54        action: commands::credentials::CredentialsCommand,
55    },
56    /// Run a code generator (omit the name, or pass --help, to list generators)
57    // `disable_help_flag` + `trailing_var_arg` let `--help` flow into `args` so
58    // we can render the dynamic generator list instead of clap's static help.
59    #[command(disable_help_flag = true)]
60    Generate {
61        /// Generator name followed by its arguments
62        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
63        args: Vec<String>,
64    },
65    /// Create a new Doido application
66    New {
67        /// Application name
68        name: String,
69        /// Skip interactive prompts; use flag values or defaults
70        #[arg(long)]
71        non_interactive: bool,
72        /// Database backend (prompted when omitted in interactive mode)
73        #[arg(long, value_enum)]
74        database: Option<DatabaseBackend>,
75        /// Include a doido-cable example channel and its wiring
76        #[arg(long)]
77        cable: bool,
78        /// Add doido-auth and run auth:install
79        #[arg(long)]
80        auth: bool,
81        /// API-only auth (JSON endpoints; omit for HTML sign-in/sign-up views)
82        #[arg(long)]
83        api: bool,
84        /// Cache backend (prompted when omitted in interactive mode)
85        #[arg(long, value_enum)]
86        cache: Option<CacheBackend>,
87        /// Jobs backend (prompted when omitted in interactive mode)
88        #[arg(long, value_enum)]
89        jobs: Option<JobsBackend>,
90    },
91}
92
93/// Runs the Doido CLI.
94///
95/// `routes` carries the application's router. The `server` command starts the
96/// HTTP server only when `routes` is `Some`; with `None` (e.g. the standalone
97/// `doido-generators` binary) the server is not started.
98pub async fn run(routes: Option<axum::Router>) {
99    // Greet on startup with the DOIDO banner (stderr, so stdout output like
100    // route tables stays clean). The running mode is the first non-flag arg.
101    let mode = std::env::args()
102        .skip(1)
103        .find(|a| !a.starts_with('-'))
104        .unwrap_or_else(|| "server".to_string());
105    crate::banner::print(&mode);
106
107    // Install the global tracing subscriber first so every command logs through
108    // the centralized logger. The fallback verbosity (when `RUST_LOG` is unset)
109    // comes from the `logger` section of `config/<env>.yml`; a missing or invalid
110    // config file falls back to the framework defaults.
111    let app_config = doido_controller::config::YamlConfig::load().unwrap_or_default();
112    doido_core::logger::init_with_config(&app_config.logger);
113
114    // Install project-specific inflection rules from `config/inflection.yaml`
115    // (relative to the project root) before any generator pluralizes a name.
116    // A missing file falls back to the default English rules.
117    if let Err(e) = doido_core::load_inflections(doido_core::inflector::DEFAULT_CONFIG_PATH) {
118        doido_core::tracing::warn!("{e}");
119    }
120
121    // Seed DATABASE_URL from `config/<env>.yml` before clap parses, so the SeaORM
122    // CLI under `doido db` (whose `generate entity` requires a database URL)
123    // picks up the configured database without the user exporting it by hand.
124    if std::env::args().nth(1).as_deref() == Some("db") {
125        commands::db::ensure_database_url_from_config();
126    }
127    let cli = Cli::parse();
128    match cli.command {
129        Commands::Server { port, env } => commands::server::run(routes, env, port).await,
130        Commands::Routes => {
131            // `routes` being `Some` means the app already built its router, which
132            // populated the global route table the macro registers into.
133            if routes.is_some() {
134                // The route table is this command's primary output — print it
135                // directly to stdout rather than through the logger.
136                doido_controller::print_routes();
137            } else {
138                doido_core::tracing::warn!("no routes configured");
139            }
140        }
141        Commands::Console => commands::console::run(),
142        Commands::Worker { once } => commands::worker::run(once).await,
143        Commands::Db { verbose, command } => commands::db::run(command, verbose).await,
144        Commands::Jobs { action } => commands::jobs::run(action).await,
145        Commands::Credentials { action } => commands::credentials::run(action),
146        Commands::Generate { args } => commands::generate::run(&args),
147        Commands::New {
148            name,
149            non_interactive,
150            database,
151            cable,
152            auth,
153            api,
154            cache,
155            jobs,
156        } => {
157            run_new(
158                &name,
159                NewOptions {
160                    non_interactive,
161                    database,
162                    cable,
163                    auth,
164                    api,
165                    cache,
166                    jobs,
167                },
168            );
169        }
170    }
171}