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