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}