prax_cli/commands/
init.rs1use 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
13pub 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 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 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 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_project_structure(&project_path)?;
72
73 output::step(2, 4, "Creating configuration file...");
74
75 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 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 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 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 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
125fn create_project_structure(path: &Path) -> CliResult<()> {
127 let prax_path = path.join(PRAX_DIR);
129 std::fs::create_dir_all(&prax_path)?;
130
131 let migrations_path = path.join(MIGRATIONS_DIR);
133 std::fs::create_dir_all(&migrations_path)?;
134
135 let gitkeep_path = migrations_path.join(".gitkeep");
137 std::fs::write(gitkeep_path, "")?;
138
139 let seeds_path = path.join(SEEDS_DIR);
141 std::fs::create_dir_all(&seeds_path)?;
142
143 let seeds_gitkeep_path = seeds_path.join(".gitkeep");
145 std::fs::write(seeds_gitkeep_path, "")?;
146
147 Ok(())
148}
149
150fn 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
292fn 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
317fn 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}