Skip to main content

tideway_cli/
cli.rs

1//! CLI argument definitions using clap.
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5#[derive(Parser)]
6#[command(name = "tideway")]
7#[command(author = "JD")]
8#[command(version)]
9#[command(about = "Scaffold Tideway apps and generate components", long_about = None)]
10pub struct Cli {
11    /// Output machine-readable JSON lines
12    #[arg(long, global = true, default_value = "false")]
13    pub json: bool,
14
15    /// Show planned changes without writing files
16    #[arg(long, global = true, default_value = "false")]
17    pub plan: bool,
18
19    #[command(subcommand)]
20    pub command: Commands,
21}
22
23#[derive(Subcommand)]
24pub enum Commands {
25    /// Create a new Tideway starter app
26    New(NewArgs),
27
28    /// Diagnose feature and project setup issues
29    Doctor(DoctorArgs),
30
31    /// Generate frontend components
32    Generate(GenerateArgs),
33
34    /// Generate backend scaffolding (routes, entities, migrations)
35    Backend(BackendArgs),
36
37    /// Add Tideway features and scaffolding to an existing project
38    Add(AddArgs),
39
40    /// Initialize main.rs by scanning for modules and wiring them together
41    Init(InitArgs),
42
43    /// Generate a CRUD resource module
44    Resource(ResourceArgs),
45
46    /// Set up frontend dependencies (Tailwind, shadcn components, etc.)
47    Setup(SetupArgs),
48
49    /// Run a Tideway app in dev mode (loads env, optional migrations)
50    Dev(DevArgs),
51
52    /// Run database migrations
53    Migrate(MigrateArgs),
54
55    /// List available templates
56    Templates,
57}
58
59#[derive(Parser, Debug)]
60pub struct NewArgs {
61    /// Project name (used for Cargo.toml)
62    #[arg(value_name = "NAME")]
63    pub name: Option<String>,
64
65    /// Preset to apply (preselect features and scaffolding)
66    #[arg(long, value_enum)]
67    pub preset: Option<NewPreset>,
68
69    /// Tideway features to enable (comma-separated)
70    #[arg(long, value_delimiter = ',')]
71    pub features: Vec<String>,
72
73    /// Generate config.rs and error.rs starter files
74    #[arg(long, default_value = "false")]
75    pub with_config: bool,
76
77    /// Generate docker-compose.yml for local Postgres
78    #[arg(long, default_value = "false")]
79    pub with_docker: bool,
80
81    /// Generate GitHub Actions CI workflow
82    #[arg(long, default_value = "false")]
83    pub with_ci: bool,
84
85    /// Skip interactive prompts (use flags instead)
86    #[arg(long, default_value = "false")]
87    pub no_prompt: bool,
88
89    /// Print a summary of generated files
90    #[arg(long, default_value = "true")]
91    pub summary: bool,
92
93    /// Always generate .env.example
94    #[arg(long, default_value = "false")]
95    pub with_env: bool,
96
97    /// Output directory (defaults to the project name)
98    #[arg(short, long)]
99    pub path: Option<String>,
100
101    /// Overwrite existing files without prompting
102    #[arg(long, default_value = "false")]
103    pub force: bool,
104}
105
106#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
107pub enum NewPreset {
108    /// Minimal starter (no extra features)
109    Minimal,
110    /// API starter with auth, database, OpenAPI, and validation
111    Api,
112    /// Print available presets
113    List,
114}
115
116#[derive(Parser, Debug)]
117pub struct AddArgs {
118    /// Feature to add (auth, database, openapi, validation, cache, sessions, jobs, websocket, metrics, email)
119    #[arg(value_enum)]
120    pub feature: AddFeature,
121
122    /// Project directory to update
123    #[arg(short, long, default_value = ".")]
124    pub path: String,
125
126    /// Overwrite existing scaffold files
127    #[arg(long, default_value = "false")]
128    pub force: bool,
129
130    /// Attempt to wire the new feature into src/main.rs
131    #[arg(long, default_value = "false")]
132    pub wire: bool,
133}
134
135#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
136pub enum AddFeature {
137    Auth,
138    Database,
139    Openapi,
140    Validation,
141    Cache,
142    Sessions,
143    Jobs,
144    Websocket,
145    Metrics,
146    Email,
147}
148
149#[derive(Parser, Debug)]
150pub struct DoctorArgs {
151    /// Project directory to analyze
152    #[arg(short, long, default_value = ".")]
153    pub path: String,
154
155    /// Generate missing .env.example when possible
156    #[arg(long, default_value = "false")]
157    pub fix: bool,
158}
159
160#[derive(Parser, Debug)]
161pub struct SetupArgs {
162    /// Frontend framework
163    #[arg(value_enum, default_value = "vue")]
164    pub framework: Framework,
165
166    /// Styling approach
167    #[arg(short, long, default_value = "shadcn")]
168    pub style: Style,
169
170    /// Skip Tailwind CSS setup
171    #[arg(long, default_value = "false")]
172    pub no_tailwind: bool,
173
174    /// Skip shadcn component installation
175    #[arg(long, default_value = "false")]
176    pub no_components: bool,
177}
178
179#[derive(Parser, Debug)]
180pub struct DevArgs {
181    /// Project directory to run
182    #[arg(short, long, default_value = ".")]
183    pub path: String,
184
185    /// Skip loading .env
186    #[arg(long, default_value = "false")]
187    pub no_env: bool,
188
189    /// Create .env from .env.example when missing
190    #[arg(long, default_value = "false")]
191    pub fix_env: bool,
192
193    /// Skip setting DATABASE_AUTO_MIGRATE=true
194    #[arg(long, default_value = "false")]
195    pub no_migrate: bool,
196
197    /// Extra args passed to `cargo run`
198    #[arg(trailing_var_arg = true)]
199    pub args: Vec<String>,
200}
201
202#[derive(Parser, Debug)]
203pub struct MigrateArgs {
204    /// Action to run (up, down, status, reset, ...)
205    #[arg(value_name = "ACTION", default_value = "up")]
206    pub action: String,
207
208    /// Project directory
209    #[arg(short, long, default_value = ".")]
210    pub path: String,
211
212    /// Migration backend
213    #[arg(long, value_enum, default_value = "auto")]
214    pub backend: MigrateBackend,
215
216    /// Skip loading .env
217    #[arg(long, default_value = "false")]
218    pub no_env: bool,
219
220    /// Create .env from .env.example when missing
221    #[arg(long, default_value = "false")]
222    pub fix_env: bool,
223
224    /// Extra args passed to the backend CLI (use `--` before them)
225    #[arg(trailing_var_arg = true)]
226    pub args: Vec<String>,
227}
228
229#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
230pub enum MigrateBackend {
231    /// Auto-detect backend from Cargo.toml
232    Auto,
233    /// SeaORM migrations via sea-orm-cli
234    SeaOrm,
235}
236
237#[derive(Parser, Debug)]
238pub struct InitArgs {
239    /// Source directory to scan for modules
240    #[arg(short, long, default_value = "./src")]
241    pub src: String,
242
243    /// Project name (defaults to directory name or Cargo.toml package name)
244    #[arg(short, long)]
245    pub name: Option<String>,
246
247    /// Overwrite existing main.rs without prompting
248    #[arg(long, default_value = "false")]
249    pub force: bool,
250
251    /// Skip database setup
252    #[arg(long, default_value = "false")]
253    pub no_database: bool,
254
255    /// Skip migration setup
256    #[arg(long, default_value = "false")]
257    pub no_migrations: bool,
258
259    /// Generate .env.example file
260    #[arg(long, default_value = "true")]
261    pub env_example: bool,
262
263    /// Generate a minimal app entrypoint and sample route
264    #[arg(long, default_value = "false")]
265    pub minimal: bool,
266}
267
268#[derive(Parser, Debug)]
269pub struct ResourceArgs {
270    /// Resource name (singular, e.g. user or invoice_item)
271    #[arg(value_name = "NAME")]
272    pub name: String,
273
274    /// Project directory
275    #[arg(short, long, default_value = ".")]
276    pub path: String,
277
278    /// Wire the module into routes/mod.rs and main.rs
279    #[arg(long, default_value = "false")]
280    pub wire: bool,
281
282    /// Generate tests
283    #[arg(long, default_value = "true")]
284    pub with_tests: bool,
285
286    /// Scaffold database entity + migration for the resource
287    #[arg(long, default_value = "false")]
288    pub db: bool,
289
290    /// Generate a repository layer for DB-backed resources
291    #[arg(long, default_value = "false")]
292    pub repo: bool,
293
294    /// Generate repository tests (requires --repo)
295    #[arg(long, default_value = "false")]
296    pub repo_tests: bool,
297
298    /// Generate a service layer (requires --repo)
299    #[arg(long, default_value = "false")]
300    pub service: bool,
301
302    /// ID type for DB scaffolding
303    #[arg(long, value_enum, default_value = "int")]
304    pub id_type: ResourceIdType,
305
306    /// Auto-add uuid dependency when using --id-type uuid
307    #[arg(long, default_value = "false")]
308    pub add_uuid: bool,
309
310    /// Add pagination (limit/offset) helpers for DB-backed resources
311    #[arg(long, default_value = "false")]
312    pub paginate: bool,
313
314    /// Add simple search filter for list endpoints (requires --paginate)
315    #[arg(long, default_value = "false")]
316    pub search: bool,
317
318    /// Database backend for scaffolding
319    #[arg(long, value_enum, default_value = "auto")]
320    pub db_backend: DbBackend,
321}
322
323#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
324pub enum ResourceIdType {
325    /// Auto-incrementing integer IDs
326    Int,
327    /// UUID IDs
328    Uuid,
329}
330
331#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
332pub enum DbBackend {
333    /// Auto-detect backend from Cargo.toml
334    Auto,
335    /// SeaORM entities + migrations
336    SeaOrm,
337}
338
339#[derive(Parser, Debug)]
340pub struct BackendArgs {
341    /// Preset: b2c (auth + billing + admin) or b2b (includes organizations)
342    #[arg(value_enum)]
343    pub preset: BackendPreset,
344
345    /// Project name (used for module naming)
346    #[arg(short, long, default_value = "my_app")]
347    pub name: String,
348
349    /// Output directory for generated source files
350    #[arg(short, long, default_value = "./src")]
351    pub output: String,
352
353    /// Output directory for migrations
354    #[arg(long, default_value = "./migration/src")]
355    pub migrations_output: String,
356
357    /// Overwrite existing files without prompting
358    #[arg(long, default_value = "false")]
359    pub force: bool,
360
361    /// Database type
362    #[arg(long, default_value = "postgres", value_parser = ["postgres", "sqlite"])]
363    pub database: String,
364}
365
366#[derive(ValueEnum, Clone, Debug, PartialEq)]
367pub enum BackendPreset {
368    /// B2C: Auth + Billing + Admin (no organizations)
369    B2c,
370    /// B2B: Auth + Billing + Organizations + Admin
371    B2b,
372}
373
374#[derive(Parser, Debug)]
375pub struct GenerateArgs {
376    /// Module to generate (auth, billing, organizations, or all)
377    #[arg(value_enum)]
378    pub module: Module,
379
380    /// Frontend framework to use
381    #[arg(short, long, default_value = "vue")]
382    pub framework: Framework,
383
384    /// Styling approach
385    #[arg(short, long, default_value = "shadcn")]
386    pub style: Style,
387
388    /// Output directory for generated files
389    #[arg(short, long, default_value = "./src/components/tideway")]
390    pub output: String,
391
392    /// API base URL for fetch calls (fallback if VITE_API_URL env var not set)
393    #[arg(long, default_value = "http://localhost:3000")]
394    pub api_base: String,
395
396    /// Overwrite existing files without prompting
397    #[arg(long, default_value = "false")]
398    pub force: bool,
399
400    /// Skip generating shared files (useApi.ts, types/index.ts)
401    #[arg(long, default_value = "false")]
402    pub no_shared: bool,
403
404    /// Also generate view files (e.g., AdminLayout.vue, AdminUsersView.vue)
405    #[arg(long, default_value = "false")]
406    pub with_views: bool,
407
408    /// Output directory for view files (only used with --with-views)
409    #[arg(long, default_value = "./src/views")]
410    pub views_output: String,
411}
412
413#[derive(ValueEnum, Clone, Debug, PartialEq)]
414pub enum Module {
415    /// Authentication components (login, register, password reset, MFA)
416    Auth,
417    /// Billing components (subscription, checkout, portal, invoices)
418    Billing,
419    /// Organization components (switcher, settings, members, invites)
420    Organizations,
421    /// Admin components (dashboard, users, organizations, impersonation)
422    Admin,
423    /// Generate all modules
424    All,
425}
426
427#[derive(ValueEnum, Clone, Debug, PartialEq)]
428pub enum Framework {
429    /// Vue 3 with Composition API
430    Vue,
431    // Future: React, Svelte
432}
433
434#[derive(ValueEnum, Clone, Debug, PartialEq)]
435pub enum Style {
436    /// shadcn-vue components (recommended)
437    Shadcn,
438    /// Plain Tailwind CSS
439    Tailwind,
440    /// Minimal HTML, no styling
441    Unstyled,
442}
443
444impl std::fmt::Display for Module {
445    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446        match self {
447            Module::Auth => write!(f, "auth"),
448            Module::Billing => write!(f, "billing"),
449            Module::Organizations => write!(f, "organizations"),
450            Module::Admin => write!(f, "admin"),
451            Module::All => write!(f, "all"),
452        }
453    }
454}
455
456impl std::fmt::Display for Framework {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        match self {
459            Framework::Vue => write!(f, "vue"),
460        }
461    }
462}
463
464impl std::fmt::Display for Style {
465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466        match self {
467            Style::Shadcn => write!(f, "shadcn"),
468            Style::Tailwind => write!(f, "tailwind"),
469            Style::Unstyled => write!(f, "unstyled"),
470        }
471    }
472}
473
474impl std::fmt::Display for BackendPreset {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        match self {
477            BackendPreset::B2c => write!(f, "b2c"),
478            BackendPreset::B2b => write!(f, "b2b"),
479        }
480    }
481}
482
483impl std::fmt::Display for AddFeature {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        let name = match self {
486            AddFeature::Auth => "auth",
487            AddFeature::Database => "database",
488            AddFeature::Openapi => "openapi",
489            AddFeature::Validation => "validation",
490            AddFeature::Cache => "cache",
491            AddFeature::Sessions => "sessions",
492            AddFeature::Jobs => "jobs",
493            AddFeature::Websocket => "websocket",
494            AddFeature::Metrics => "metrics",
495            AddFeature::Email => "email",
496        };
497        write!(f, "{}", name)
498    }
499}