Skip to main content

doido_generators/
cli.rs

1use crate::commands::{self, jobs::JobsCommand, new::run_new};
2use crate::new_options::{CacheBackend, DatabaseBackend, JobsBackend};
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        /// Cache backend (prompted when omitted in interactive mode)
79        #[arg(long, value_enum)]
80        cache: Option<CacheBackend>,
81        /// Jobs backend (prompted when omitted in interactive mode)
82        #[arg(long, value_enum)]
83        jobs: Option<JobsBackend>,
84    },
85}
86
87/// Runs the Doido CLI.
88///
89/// `routes` carries the application's router. The `server` command starts the
90/// HTTP server only when `routes` is `Some`; with `None` (e.g. the standalone
91/// `doido-generators` binary) the server is not started.
92pub async fn run(routes: Option<axum::Router>) {
93    // Greet on startup with the DOIDO banner (stderr, so stdout output like
94    // route tables stays clean). The running mode is the first non-flag arg.
95    let mode = std::env::args()
96        .skip(1)
97        .find(|a| !a.starts_with('-'))
98        .unwrap_or_else(|| "server".to_string());
99    crate::banner::print(&mode);
100
101    // Install the global tracing subscriber first so every command logs through
102    // the centralized logger. The fallback verbosity (when `RUST_LOG` is unset)
103    // comes from the `logger` section of `config/<env>.yml`; a missing or invalid
104    // config file falls back to the framework defaults.
105    let app_config = doido_controller::config::YamlConfig::load().unwrap_or_default();
106    doido_core::logger::init_with_config(&app_config.logger);
107
108    // Install project-specific inflection rules from `config/inflection.yaml`
109    // (relative to the project root) before any generator pluralizes a name.
110    // A missing file falls back to the default English rules.
111    if let Err(e) = doido_core::load_inflections(doido_core::inflector::DEFAULT_CONFIG_PATH) {
112        doido_core::tracing::warn!("{e}");
113    }
114
115    // Seed DATABASE_URL from `config/<env>.yml` before clap parses, so the SeaORM
116    // CLI under `doido db` (whose `generate entity` requires a database URL)
117    // picks up the configured database without the user exporting it by hand.
118    if std::env::args().nth(1).as_deref() == Some("db") {
119        commands::db::ensure_database_url_from_config();
120    }
121    let cli = Cli::parse();
122    match cli.command {
123        Commands::Server { port, env } => commands::server::run(routes, env, port).await,
124        Commands::Routes => {
125            // `routes` being `Some` means the app already built its router, which
126            // populated the global route table the macro registers into.
127            if routes.is_some() {
128                // The route table is this command's primary output — print it
129                // directly to stdout rather than through the logger.
130                doido_controller::print_routes();
131            } else {
132                doido_core::tracing::warn!("no routes configured");
133            }
134        }
135        Commands::Console => commands::console::run(),
136        Commands::Worker { once } => commands::worker::run(once).await,
137        Commands::Db { verbose, command } => commands::db::run(command, verbose).await,
138        Commands::Jobs { action } => commands::jobs::run(action).await,
139        Commands::Credentials { action } => commands::credentials::run(action),
140        Commands::Generate { args } => commands::generate::run(&args),
141        Commands::New {
142            name,
143            non_interactive,
144            database,
145            cable,
146            cache,
147            jobs,
148        } => {
149            run_new(&name, non_interactive, database, cable, cache, jobs);
150        }
151    }
152}