1use std::ffi::OsString;
2use std::path::{Path, PathBuf};
3
4use clap::{Parser, Subcommand, ValueEnum};
5use clap_complete::Shell;
6
7use crate::config::{CliConfig, ConfigError};
8use crate::db::DbCliError;
9
10#[derive(Debug, Parser)]
11#[command(name = "rustauth", version, about = "Command-line tools for RustAuth.")]
12pub struct Cli {
13 #[arg(short = 'c', long, global = true, default_value = ".")]
14 cwd: PathBuf,
15 #[arg(long, global = true)]
16 config: Option<PathBuf>,
17 #[command(subcommand)]
18 command: Commands,
19}
20
21#[derive(Debug, Subcommand)]
22pub(crate) enum Commands {
23 Init(InitArgs),
24 Doctor(DiagnosticArgs),
25 Info(InfoArgs),
26 Secret(SecretArgs),
27 Db(DbArgs),
28 Generate(GenerateArgs),
29 Migrate(MigrateArgs),
30 Schema(SchemaArgs),
31 Plugins(PluginsArgs),
32 Completions(CompletionsArgs),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
36pub(crate) enum InitFramework {
37 Axum,
38 #[value(name = "actix-web", alias = "actix_web")]
39 ActixWeb,
40}
41
42impl InitFramework {
43 pub(crate) fn as_config_str(self) -> &'static str {
44 match self {
45 Self::Axum => "axum",
46 Self::ActixWeb => "actix-web",
47 }
48 }
49}
50
51#[derive(Debug, clap::Args)]
52pub(crate) struct InitArgs {
53 #[arg(long, required = true)]
55 pub(crate) framework: InitFramework,
56 #[arg(long)]
57 pub(crate) adapter: Option<String>,
58 #[arg(long)]
59 pub(crate) database: Option<String>,
60 #[arg(long)]
61 pub(crate) base_url: Option<String>,
62 #[arg(long, value_delimiter = ',')]
63 pub(crate) plugins: Vec<String>,
64 #[arg(short = 'y', long)]
65 pub(crate) yes: bool,
66 #[arg(long)]
67 pub(crate) force: bool,
68 #[arg(long)]
70 pub(crate) seed_secrets: bool,
71}
72
73#[derive(Debug, clap::Args)]
74pub(crate) struct DiagnosticArgs {
75 #[arg(long)]
76 pub(crate) production: bool,
77 #[arg(long)]
78 pub(crate) json: bool,
79 #[arg(long)]
80 pub(crate) strict: bool,
81}
82
83#[derive(Debug, clap::Args)]
84pub(crate) struct InfoArgs {
85 #[arg(short = 'j', long)]
86 pub(crate) json: bool,
87 #[arg(short = 'C', long)]
88 pub(crate) copy: bool,
89}
90
91#[derive(Debug, clap::Args)]
92pub(crate) struct SecretArgs {
93 #[arg(long, default_value_t = 32)]
94 pub(crate) bytes: usize,
95 #[arg(long)]
96 pub(crate) check: Option<String>,
97 #[arg(long)]
98 pub(crate) check_env: Option<String>,
99 #[arg(long)]
100 pub(crate) env_line: bool,
101 #[arg(long, default_value_t = true)]
103 pub(crate) production: bool,
104 #[arg(long, conflicts_with = "production")]
106 pub(crate) dev: bool,
107}
108
109#[derive(Debug, clap::Args)]
110pub(crate) struct DbArgs {
111 #[command(subcommand)]
112 pub(crate) command: DbCommands,
113}
114
115#[derive(Debug, Subcommand)]
116pub(crate) enum DbCommands {
117 Status(StatusArgs),
118 Generate(GenerateArgs),
119 Migrate(MigrateArgs),
120}
121
122#[derive(Debug, clap::Args)]
123pub(crate) struct StatusArgs {
124 #[arg(long)]
125 pub(crate) json: bool,
126 #[arg(long)]
127 pub(crate) check: bool,
128}
129
130#[derive(Debug, clap::Args)]
131pub(crate) struct GenerateArgs {
132 #[arg(long)]
133 pub(crate) output: Option<PathBuf>,
134 #[arg(long)]
135 pub(crate) output_dir: Option<PathBuf>,
136 #[arg(long)]
137 pub(crate) adapter: Option<String>,
138 #[arg(long)]
139 pub(crate) dialect: Option<String>,
140 #[arg(long)]
141 pub(crate) from_empty: bool,
142 #[arg(long)]
143 pub(crate) force: bool,
144 #[arg(short = 'y', long)]
145 pub(crate) yes: bool,
146}
147
148#[derive(Debug, clap::Args)]
149pub(crate) struct MigrateArgs {
150 #[arg(long)]
151 pub(crate) dry_run: bool,
152 #[arg(short = 'y', long)]
153 pub(crate) yes: bool,
154}
155
156#[derive(Debug, clap::Args)]
157pub(crate) struct SchemaArgs {
158 #[command(subcommand)]
159 pub(crate) command: SchemaCommands,
160}
161
162#[derive(Debug, Subcommand)]
163pub(crate) enum SchemaCommands {
164 Print(SchemaPrintArgs),
165}
166
167#[derive(Debug, clap::Args)]
168pub(crate) struct SchemaPrintArgs {
169 #[arg(long, value_enum, default_value_t = SchemaFormat::Sql)]
170 pub(crate) format: SchemaFormat,
171 #[arg(long, default_value = "sqlite")]
172 pub(crate) dialect: String,
173}
174
175#[derive(Debug, Clone, Copy, ValueEnum)]
176pub(crate) enum SchemaFormat {
177 Sql,
178 Json,
179}
180
181#[derive(Debug, clap::Args)]
182pub(crate) struct PluginsArgs {
183 #[command(subcommand)]
184 pub(crate) command: PluginsCommands,
185}
186
187#[derive(Debug, Subcommand)]
188pub(crate) enum PluginsCommands {
189 List(PluginListArgs),
190 Add(PluginChangeArgs),
191 Remove(PluginChangeArgs),
192}
193
194#[derive(Debug, clap::Args)]
195pub(crate) struct PluginListArgs {
196 #[arg(long)]
197 pub(crate) json: bool,
198}
199
200#[derive(Debug, clap::Args)]
201pub(crate) struct PluginChangeArgs {
202 pub(crate) plugin: String,
203 #[arg(short = 'y', long)]
204 pub(crate) yes: bool,
205}
206
207#[derive(Debug, clap::Args)]
208pub(crate) struct CompletionsArgs {
209 pub(crate) shell: Shell,
210}
211
212pub fn run() -> i32 {
213 run_from(std::env::args_os())
214}
215
216pub fn run_cargo() -> i32 {
217 let mut args = std::env::args_os().collect::<Vec<_>>();
218 if args
219 .get(1)
220 .and_then(|arg| arg.to_str())
221 .is_some_and(is_cargo_subcommand_name)
222 {
223 args.remove(1);
224 }
225 run_from(args)
226}
227
228fn is_cargo_subcommand_name(value: &str) -> bool {
229 matches!(
230 value,
231 "rustauth" | "rust-auth" | "better-auth" | "betterauth"
232 )
233}
234
235pub fn run_from<I, T>(args: I) -> i32
236where
237 I: IntoIterator<Item = T>,
238 T: Into<OsString> + Clone,
239{
240 match Cli::try_parse_from(args) {
241 Ok(cli) => match execute(cli) {
242 Ok(()) => 0,
243 Err(AppError::SilentExit { code }) => code,
244 Err(error) => {
245 eprintln!("{error}");
246 1
247 }
248 },
249 Err(error) => {
250 let _ = error.print();
251 error.exit_code()
252 }
253 }
254}
255
256fn execute(cli: Cli) -> Result<(), AppError> {
257 let runtime = tokio::runtime::Runtime::new().map_err(AppError::Runtime)?;
258 runtime.block_on(async move { execute_async(cli).await })
259}
260
261async fn execute_async(cli: Cli) -> Result<(), AppError> {
262 let cwd = crate::paths::absolute_cwd(&cli.cwd)?;
263 let config_path = crate::paths::resolve_config_path(&cwd, cli.config.as_deref());
264 crate::env::load_project_env(&cwd, &config_path)?;
265 let context = AppContext { config_path, cwd };
266 match cli.command {
267 Commands::Init(args) => crate::commands::init::run(&context, args),
268 Commands::Doctor(args) => crate::commands::doctor::run(&context, args).await,
269 Commands::Info(args) => crate::commands::info::run(&context, args).await,
270 Commands::Secret(args) => crate::commands::secret::run(args),
271 Commands::Db(args) => match args.command {
272 DbCommands::Status(args) => crate::commands::db::status(&context, args).await,
273 DbCommands::Generate(args) => crate::commands::db::generate(&context, args).await,
274 DbCommands::Migrate(args) => crate::commands::db::migrate(&context, args).await,
275 },
276 Commands::Generate(args) => crate::commands::db::generate(&context, args).await,
277 Commands::Migrate(args) => crate::commands::db::migrate(&context, args).await,
278 Commands::Schema(args) => match args.command {
279 SchemaCommands::Print(args) => crate::commands::schema::print(&context, args),
280 },
281 Commands::Plugins(args) => match args.command {
282 PluginsCommands::List(args) => crate::commands::plugins::list(args),
283 PluginsCommands::Add(args) => crate::commands::plugins::add(&context, args).await,
284 PluginsCommands::Remove(args) => crate::commands::plugins::remove(&context, args),
285 },
286 Commands::Completions(args) => crate::commands::completions::run(args),
287 }
288}
289
290pub(crate) struct AppContext {
291 cwd: PathBuf,
292 config_path: PathBuf,
293}
294
295impl AppContext {
296 pub(crate) fn cwd(&self) -> &Path {
297 &self.cwd
298 }
299
300 pub(crate) fn config_path(&self) -> &Path {
301 &self.config_path
302 }
303
304 pub(crate) fn load_config(&self) -> Result<CliConfig, AppError> {
305 CliConfig::load(&self.config_path).map_err(|error| match error {
306 ConfigError::Read { path, source }
307 if source.kind() == std::io::ErrorKind::NotFound =>
308 {
309 AppError::Message(format!(
310 "No RustAuth CLI config found at {}. Run `rustauth init --framework axum` or `rustauth init --framework actix-web`, or pass --config <path>.",
311 path.display()
312 ))
313 }
314 other => AppError::Config(other),
315 })
316 }
317
318 pub(crate) fn load_config_or_default(&self) -> Result<(CliConfig, bool), AppError> {
324 match CliConfig::load_optional(&self.config_path)? {
325 Some(config) => Ok((config, true)),
326 None => Ok((CliConfig::default(), false)),
327 }
328 }
329
330 pub(crate) fn resolve_project_path(&self, path: &Path) -> PathBuf {
331 crate::paths::resolve_project_path(&self.cwd, path)
332 }
333}
334
335#[derive(Debug, thiserror::Error)]
336pub(crate) enum AppError {
337 #[error("{0}")]
338 Message(String),
339 #[error(transparent)]
340 Config(#[from] ConfigError),
341 #[error(transparent)]
342 Db(#[from] DbCliError),
343 #[error(transparent)]
344 RustAuth(#[from] rustauth_core::error::RustAuthError),
345 #[error(transparent)]
346 Json(#[from] serde_json::Error),
347 #[error("failed to start async runtime: {0}")]
348 Runtime(std::io::Error),
349 #[error("{context}: {source}")]
350 Io {
351 context: String,
352 source: std::io::Error,
353 },
354 #[error("command exited with status {code}")]
355 SilentExit { code: i32 },
356}