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