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    /// SaaS starter with B2B backend modules and production defaults
113    Saas,
114    /// Worker starter focused on background jobs
115    Worker,
116    /// Print available presets
117    List,
118}
119
120#[derive(Parser, Debug)]
121pub struct AddArgs {
122    /// Feature to add (auth, database, openapi, validation, cache, sessions, jobs, websocket, metrics, email)
123    #[arg(value_enum)]
124    pub feature: AddFeature,
125
126    /// Project directory to update
127    #[arg(short, long, default_value = ".")]
128    pub path: String,
129
130    /// Overwrite existing scaffold files
131    #[arg(long, default_value = "false")]
132    pub force: bool,
133
134    /// Attempt to wire the new feature into src/main.rs
135    #[arg(long, default_value = "false")]
136    pub wire: bool,
137}
138
139#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
140pub enum AddFeature {
141    Auth,
142    Database,
143    Openapi,
144    Validation,
145    Cache,
146    Sessions,
147    Jobs,
148    Websocket,
149    Metrics,
150    Email,
151}
152
153#[derive(Parser, Debug)]
154pub struct DoctorArgs {
155    /// Project directory to analyze
156    #[arg(short, long, default_value = ".")]
157    pub path: String,
158
159    /// Generate missing .env.example when possible
160    #[arg(long, default_value = "false")]
161    pub fix: bool,
162}
163
164#[derive(Parser, Debug)]
165pub struct SetupArgs {
166    /// Frontend framework
167    #[arg(value_enum, default_value = "vue")]
168    pub framework: Framework,
169
170    /// Styling approach
171    #[arg(short, long, default_value = "shadcn")]
172    pub style: Style,
173
174    /// Skip Tailwind CSS setup
175    #[arg(long, default_value = "false")]
176    pub no_tailwind: bool,
177
178    /// Skip shadcn component installation
179    #[arg(long, default_value = "false")]
180    pub no_components: bool,
181}
182
183#[derive(Parser, Debug)]
184pub struct DevArgs {
185    /// Project directory to run
186    #[arg(short, long, default_value = ".")]
187    pub path: String,
188
189    /// Skip loading .env
190    #[arg(long, default_value = "false")]
191    pub no_env: bool,
192
193    /// Create .env from .env.example when missing
194    #[arg(long, default_value = "false")]
195    pub fix_env: bool,
196
197    /// Skip setting DATABASE_AUTO_MIGRATE=true
198    #[arg(long, default_value = "false")]
199    pub no_migrate: bool,
200
201    /// Extra args passed to `cargo run`
202    #[arg(trailing_var_arg = true)]
203    pub args: Vec<String>,
204}
205
206#[derive(Parser, Debug)]
207pub struct MigrateArgs {
208    /// Action to run (up, down, status, reset, ...)
209    #[arg(value_name = "ACTION", default_value = "up")]
210    pub action: String,
211
212    /// Project directory
213    #[arg(short, long, default_value = ".")]
214    pub path: String,
215
216    /// Migration backend
217    #[arg(long, value_enum, default_value = "auto")]
218    pub backend: MigrateBackend,
219
220    /// Skip loading .env
221    #[arg(long, default_value = "false")]
222    pub no_env: bool,
223
224    /// Create .env from .env.example when missing
225    #[arg(long, default_value = "false")]
226    pub fix_env: bool,
227
228    /// Extra args passed to the backend CLI (use `--` before them)
229    #[arg(trailing_var_arg = true)]
230    pub args: Vec<String>,
231}
232
233#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
234pub enum MigrateBackend {
235    /// Auto-detect backend from Cargo.toml
236    Auto,
237    /// SeaORM migrations via sea-orm-cli
238    SeaOrm,
239}
240
241#[derive(Parser, Debug)]
242pub struct InitArgs {
243    /// Source directory to scan for modules
244    #[arg(short, long, default_value = "./src")]
245    pub src: String,
246
247    /// Project name (defaults to directory name or Cargo.toml package name)
248    #[arg(short, long)]
249    pub name: Option<String>,
250
251    /// Overwrite existing main.rs without prompting
252    #[arg(long, default_value = "false")]
253    pub force: bool,
254
255    /// Skip database setup
256    #[arg(long, default_value = "false")]
257    pub no_database: bool,
258
259    /// Skip migration setup
260    #[arg(long, default_value = "false")]
261    pub no_migrations: bool,
262
263    /// Generate .env.example file
264    #[arg(long, default_value = "true")]
265    pub env_example: bool,
266
267    /// Generate a minimal app entrypoint and sample route
268    #[arg(long, default_value = "false")]
269    pub minimal: bool,
270}
271
272#[derive(Parser, Debug)]
273pub struct ResourceArgs {
274    /// Resource name (singular, e.g. user or invoice_item)
275    #[arg(value_name = "NAME")]
276    pub name: String,
277
278    /// Project directory
279    #[arg(short, long, default_value = ".")]
280    pub path: String,
281
282    /// Wire the module into routes/mod.rs and main.rs
283    #[arg(long, default_value = "false")]
284    pub wire: bool,
285
286    /// Generate tests
287    #[arg(long, default_value = "true")]
288    pub with_tests: bool,
289
290    /// Scaffold database entity + migration for the resource
291    #[arg(long, default_value = "false")]
292    pub db: bool,
293
294    /// Generate a repository layer for DB-backed resources
295    #[arg(long, default_value = "false")]
296    pub repo: bool,
297
298    /// Generate repository tests (requires --repo)
299    #[arg(long, default_value = "false")]
300    pub repo_tests: bool,
301
302    /// Generate a service layer (requires --repo)
303    #[arg(long, default_value = "false")]
304    pub service: bool,
305
306    /// ID type for DB scaffolding
307    #[arg(long, value_enum, default_value = "int")]
308    pub id_type: ResourceIdType,
309
310    /// Auto-add uuid dependency when using --id-type uuid
311    #[arg(long, default_value = "false")]
312    pub add_uuid: bool,
313
314    /// Add pagination (limit/offset) helpers for DB-backed resources
315    #[arg(long, default_value = "false")]
316    pub paginate: bool,
317
318    /// Add simple search filter for list endpoints (requires --paginate)
319    #[arg(long, default_value = "false")]
320    pub search: bool,
321
322    /// Database backend for scaffolding
323    #[arg(long, value_enum, default_value = "auto")]
324    pub db_backend: DbBackend,
325}
326
327#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
328pub enum ResourceIdType {
329    /// Auto-incrementing integer IDs
330    Int,
331    /// UUID IDs
332    Uuid,
333}
334
335#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
336pub enum DbBackend {
337    /// Auto-detect backend from Cargo.toml
338    Auto,
339    /// SeaORM entities + migrations
340    SeaOrm,
341}
342
343#[derive(Parser, Debug)]
344pub struct BackendArgs {
345    /// Preset: b2c (auth + billing + admin) or b2b (includes organizations)
346    #[arg(value_enum)]
347    pub preset: BackendPreset,
348
349    /// Project name (used for module naming)
350    #[arg(short, long, default_value = "my_app")]
351    pub name: String,
352
353    /// Output directory for generated source files
354    #[arg(short, long, default_value = "./src")]
355    pub output: String,
356
357    /// Output directory for migrations
358    #[arg(long, default_value = "./migration/src")]
359    pub migrations_output: String,
360
361    /// Overwrite existing files without prompting
362    #[arg(long, default_value = "false")]
363    pub force: bool,
364
365    /// Database type
366    #[arg(long, default_value = "postgres", value_parser = ["postgres", "sqlite"])]
367    pub database: String,
368}
369
370#[derive(ValueEnum, Clone, Debug, PartialEq)]
371pub enum BackendPreset {
372    /// B2C: Auth + Billing + Admin (no organizations)
373    B2c,
374    /// B2B: Auth + Billing + Organizations + Admin
375    B2b,
376}
377
378#[derive(Parser, Debug)]
379pub struct GenerateArgs {
380    /// Module to generate (auth, billing, organizations, or all)
381    #[arg(value_enum)]
382    pub module: Module,
383
384    /// Frontend framework to use
385    #[arg(short, long, default_value = "vue")]
386    pub framework: Framework,
387
388    /// Styling approach
389    #[arg(short, long, default_value = "shadcn")]
390    pub style: Style,
391
392    /// Output directory for generated files
393    #[arg(short, long, default_value = "./src/components/tideway")]
394    pub output: String,
395
396    /// API base URL for fetch calls (fallback if VITE_API_URL env var not set)
397    #[arg(long, default_value = "http://localhost:3000")]
398    pub api_base: String,
399
400    /// Overwrite existing files without prompting
401    #[arg(long, default_value = "false")]
402    pub force: bool,
403
404    /// Skip generating shared files (useApi.ts, types/index.ts)
405    #[arg(long, default_value = "false")]
406    pub no_shared: bool,
407
408    /// Also generate view files (e.g., AdminLayout.vue, AdminUsersView.vue)
409    #[arg(long, default_value = "false")]
410    pub with_views: bool,
411
412    /// Output directory for view files (only used with --with-views)
413    #[arg(long, default_value = "./src/views")]
414    pub views_output: String,
415}
416
417#[derive(ValueEnum, Clone, Debug, PartialEq)]
418pub enum Module {
419    /// Authentication components (login, register, password reset, MFA)
420    Auth,
421    /// Billing components (subscription, checkout, portal, invoices)
422    Billing,
423    /// Organization components (switcher, settings, members, invites)
424    Organizations,
425    /// Admin components (dashboard, users, organizations, impersonation)
426    Admin,
427    /// Generate all modules
428    All,
429}
430
431#[derive(ValueEnum, Clone, Debug, PartialEq)]
432pub enum Framework {
433    /// Vue 3 with Composition API
434    Vue,
435    // Future: React, Svelte
436}
437
438#[derive(ValueEnum, Clone, Debug, PartialEq)]
439pub enum Style {
440    /// shadcn-vue components (recommended)
441    Shadcn,
442    /// Plain Tailwind CSS
443    Tailwind,
444    /// Minimal HTML, no styling
445    Unstyled,
446}
447
448impl std::fmt::Display for Module {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        match self {
451            Module::Auth => write!(f, "auth"),
452            Module::Billing => write!(f, "billing"),
453            Module::Organizations => write!(f, "organizations"),
454            Module::Admin => write!(f, "admin"),
455            Module::All => write!(f, "all"),
456        }
457    }
458}
459
460impl std::fmt::Display for Framework {
461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462        match self {
463            Framework::Vue => write!(f, "vue"),
464        }
465    }
466}
467
468impl std::fmt::Display for Style {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        match self {
471            Style::Shadcn => write!(f, "shadcn"),
472            Style::Tailwind => write!(f, "tailwind"),
473            Style::Unstyled => write!(f, "unstyled"),
474        }
475    }
476}
477
478impl std::fmt::Display for BackendPreset {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        match self {
481            BackendPreset::B2c => write!(f, "b2c"),
482            BackendPreset::B2b => write!(f, "b2b"),
483        }
484    }
485}
486
487impl std::fmt::Display for AddFeature {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        let name = match self {
490            AddFeature::Auth => "auth",
491            AddFeature::Database => "database",
492            AddFeature::Openapi => "openapi",
493            AddFeature::Validation => "validation",
494            AddFeature::Cache => "cache",
495            AddFeature::Sessions => "sessions",
496            AddFeature::Jobs => "jobs",
497            AddFeature::Websocket => "websocket",
498            AddFeature::Metrics => "metrics",
499            AddFeature::Email => "email",
500        };
501        write!(f, "{}", name)
502    }
503}