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}