Skip to main content

openauth_cli/
app.rs

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