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};
9use crate::error::CliResult;
10use crate::output::{self, confirm, input, select, success};
11
12pub 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 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 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 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_project_structure(&project_path)?;
71
72 output::step(2, 4, "Creating configuration file...");
73
74 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 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 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 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 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
123fn create_project_structure(path: &Path) -> CliResult<()> {
125 let prax_path = path.join(PRAX_DIR);
127 std::fs::create_dir_all(&prax_path)?;
128
129 let migrations_path = path.join(MIGRATIONS_DIR);
131 std::fs::create_dir_all(&migrations_path)?;
132
133 let gitkeep_path = migrations_path.join(".gitkeep");
135 std::fs::write(gitkeep_path, "")?;
136
137 Ok(())
138}
139
140fn 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
282fn 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
307fn 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}