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}