Skip to main content

prax_cli/commands/
init.rs

1//! `prax init` command - Initialize a new Prax project.
2
3use std::path::Path;
4
5use crate::cli::{DatabaseProvider, InitArgs};
6use crate::config::{
7    CONFIG_FILE_NAME, Config, MIGRATIONS_DIR, PRAX_DIR, SCHEMA_FILE_NAME, SCHEMA_FILE_PATH,
8    SEEDS_DIR,
9};
10use crate::error::CliResult;
11use crate::output::{self, confirm, input, select, success};
12
13/// Run the init command
14pub async fn run(args: InitArgs) -> CliResult<()> {
15    output::header("Initialize Prax Project");
16
17    let project_path = args
18        .path
19        .canonicalize()
20        .unwrap_or_else(|_| args.path.clone());
21
22    // Check if already initialized
23    let config_path = project_path.join(CONFIG_FILE_NAME);
24    if config_path.exists() {
25        output::warn(&format!(
26            "Project already initialized. {} exists.",
27            CONFIG_FILE_NAME
28        ));
29
30        if !args.yes && !confirm("Reinitialize project?") {
31            return Ok(());
32        }
33    }
34
35    // Get database provider
36    let provider = if args.yes {
37        args.provider
38    } else {
39        let providers = ["PostgreSQL", "MySQL", "SQLite"];
40        let selection = select("Select database provider:", &providers);
41        match selection {
42            Some(0) => DatabaseProvider::Postgresql,
43            Some(1) => DatabaseProvider::Mysql,
44            Some(2) => DatabaseProvider::Sqlite,
45            _ => args.provider,
46        }
47    };
48
49    // Get database URL
50    let db_url = if args.yes {
51        args.url
52    } else {
53        let default_url = match provider {
54            DatabaseProvider::Postgresql => "postgresql://user:password@localhost:5432/mydb",
55            DatabaseProvider::Mysql => "mysql://user:password@localhost:3306/mydb",
56            DatabaseProvider::Sqlite => "file:./dev.db",
57        };
58        let prompt = format!("Database URL [{}] (or leave empty to use env)", default_url);
59        let url = input(&prompt);
60        if url.as_ref().map(|s| s.is_empty()).unwrap_or(true) {
61            None
62        } else {
63            url
64        }
65    };
66
67    output::newline();
68    output::step(1, 4, "Creating project structure...");
69
70    // Create directories
71    create_project_structure(&project_path)?;
72
73    output::step(2, 4, "Creating configuration file...");
74
75    // Create config
76    let mut config = Config::default_for_provider(&provider.to_string());
77    config.database.url = db_url.clone();
78    config.save(&config_path)?;
79
80    output::step(3, 4, "Creating schema file...");
81
82    // Create schema file in prax/ directory
83    let schema_path = project_path.join(SCHEMA_FILE_PATH);
84    if !args.no_example {
85        create_example_schema(&schema_path, provider)?;
86    } else {
87        create_minimal_schema(&schema_path, provider)?;
88    }
89
90    output::step(4, 4, "Creating .env file...");
91
92    // Create .env file
93    let env_path = project_path.join(".env");
94    if !env_path.exists() {
95        create_env_file(&env_path, provider, &db_url)?;
96    }
97
98    output::newline();
99    success("Project initialized successfully!");
100    output::newline();
101
102    // Print next steps
103    output::section("Next steps");
104    output::list_item(&format!("Edit {} to define your schema", SCHEMA_FILE_PATH));
105    output::list_item("Set your DATABASE_URL in .env");
106    output::list_item("Run `prax generate` to generate Rust code");
107    output::list_item("Run `prax migrate dev` to create your first migration");
108    output::newline();
109
110    // Show file structure
111    output::section("Created files");
112    output::kv(CONFIG_FILE_NAME, "Prax configuration (project root)");
113    output::kv(&format!("{}/", PRAX_DIR), "Prax directory");
114    output::kv(
115        &format!("  {}", SCHEMA_FILE_NAME),
116        "Database schema definition",
117    );
118    output::kv("  migrations/", "Migration files");
119    output::kv("  seeds/", "Seed files");
120    output::kv(".env", "Environment variables");
121
122    Ok(())
123}
124
125/// Create the project directory structure
126fn create_project_structure(path: &Path) -> CliResult<()> {
127    // Create prax directory
128    let prax_path = path.join(PRAX_DIR);
129    std::fs::create_dir_all(&prax_path)?;
130
131    // Create migrations directory inside prax/
132    let migrations_path = path.join(MIGRATIONS_DIR);
133    std::fs::create_dir_all(&migrations_path)?;
134
135    // Create .gitkeep in migrations
136    let gitkeep_path = migrations_path.join(".gitkeep");
137    std::fs::write(gitkeep_path, "")?;
138
139    // Create seeds directory inside prax/
140    let seeds_path = path.join(SEEDS_DIR);
141    std::fs::create_dir_all(&seeds_path)?;
142
143    // Create .gitkeep in seeds
144    let seeds_gitkeep_path = seeds_path.join(".gitkeep");
145    std::fs::write(seeds_gitkeep_path, "")?;
146
147    Ok(())
148}
149
150/// Create an example schema file
151fn create_example_schema(path: &Path, provider: DatabaseProvider) -> CliResult<()> {
152    let schema = match provider {
153        DatabaseProvider::Postgresql => {
154            r#"// Prax Schema File
155// Learn more at https://prax.dev/docs/schema
156
157// Database connection
158datasource db {
159    provider = "postgresql"
160    url      = env("DATABASE_URL")
161}
162
163// Client generator
164generator client {
165    provider = "prax-client-rust"
166    output   = "./src/generated"
167}
168
169// =============================================================================
170// Example Models
171// =============================================================================
172
173/// A user in the system
174model User {
175    id        Int      @id @auto
176    email     String   @unique
177    name      String?
178    password  String   @writeonly
179    role      Role     @default(USER)
180    posts     Post[]
181    profile   Profile?
182    createdAt DateTime @default(now()) @map("created_at")
183    updatedAt DateTime @updatedAt @map("updated_at")
184
185    @@map("users")
186    @@index([email])
187}
188
189/// User profile with additional information
190model Profile {
191    id     Int     @id @auto
192    bio    String?
193    avatar String?
194    user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)
195    userId Int     @unique @map("user_id")
196
197    @@map("profiles")
198}
199
200/// A blog post
201model Post {
202    id        Int        @id @auto
203    title     String
204    content   String?
205    published Boolean    @default(false)
206    author    User       @relation(fields: [authorId], references: [id])
207    authorId  Int        @map("author_id")
208    tags      Tag[]
209    createdAt DateTime   @default(now()) @map("created_at")
210    updatedAt DateTime   @updatedAt @map("updated_at")
211
212    @@map("posts")
213    @@index([authorId])
214    @@index([published])
215}
216
217/// Tags for posts
218model Tag {
219    id    Int    @id @auto
220    name  String @unique
221    posts Post[]
222
223    @@map("tags")
224}
225
226/// User roles
227enum Role {
228    USER
229    ADMIN
230    MODERATOR
231}
232"#
233        }
234        DatabaseProvider::Mysql => {
235            r#"// Prax Schema File
236// Learn more at https://prax.dev/docs/schema
237
238datasource db {
239    provider = "mysql"
240    url      = env("DATABASE_URL")
241}
242
243generator client {
244    provider = "prax-client-rust"
245    output   = "./src/generated"
246}
247
248/// A user in the system
249model User {
250    id        Int      @id @auto
251    email     String   @unique @db.VarChar(255)
252    name      String?  @db.VarChar(100)
253    createdAt DateTime @default(now()) @map("created_at")
254    updatedAt DateTime @updatedAt @map("updated_at")
255
256    @@map("users")
257}
258"#
259        }
260        DatabaseProvider::Sqlite => {
261            r#"// Prax Schema File
262// Learn more at https://prax.dev/docs/schema
263
264datasource db {
265    provider = "sqlite"
266    url      = env("DATABASE_URL")
267}
268
269generator client {
270    provider = "prax-client-rust"
271    output   = "./src/generated"
272}
273
274/// A user in the system
275model User {
276    id        Int      @id @auto
277    email     String   @unique
278    name      String?
279    createdAt DateTime @default(now()) @map("created_at")
280    updatedAt DateTime @updatedAt @map("updated_at")
281
282    @@map("users")
283}
284"#
285        }
286    };
287
288    std::fs::write(path, schema)?;
289    Ok(())
290}
291
292/// Create a minimal schema file (without examples)
293fn create_minimal_schema(path: &Path, provider: DatabaseProvider) -> CliResult<()> {
294    let schema = format!(
295        r#"// Prax Schema File
296// Learn more at https://prax.dev/docs/schema
297
298datasource db {{
299    provider = "{}"
300    url      = env("DATABASE_URL")
301}}
302
303generator client {{
304    provider = "prax-client-rust"
305    output   = "./src/generated"
306}}
307
308// Add your models here
309"#,
310        provider
311    );
312
313    std::fs::write(path, schema)?;
314    Ok(())
315}
316
317/// Create .env file
318fn create_env_file(path: &Path, provider: DatabaseProvider, url: &Option<String>) -> CliResult<()> {
319    let default_url = match provider {
320        DatabaseProvider::Postgresql => "postgresql://user:password@localhost:5432/mydb",
321        DatabaseProvider::Mysql => "mysql://user:password@localhost:3306/mydb",
322        DatabaseProvider::Sqlite => "file:./dev.db",
323    };
324
325    let url = url.as_deref().unwrap_or(default_url);
326
327    let content = format!(
328        r#"# Database connection URL
329DATABASE_URL={}
330
331# Shadow database for migrations (optional, PostgreSQL/MySQL only)
332# SHADOW_DATABASE_URL=
333
334# Direct database URL (bypasses connection pooling)
335# DIRECT_URL=
336"#,
337        url
338    );
339
340    std::fs::write(path, content)?;
341    Ok(())
342}