Skip to content

Commit aeb7e80

Browse files
committed
WIP4
1 parent c03ba28 commit aeb7e80

1 file changed

Lines changed: 152 additions & 81 deletions

File tree

packages/catcom/src/main.rs

Lines changed: 152 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::env;
44
use std::fs;
55
use std::io::{self, BufRead, Write as _};
66
use std::path::{Path, PathBuf};
7-
use std::process::{exit, Child, Command, Stdio};
7+
use std::process::{Child, Command, Stdio, exit};
88
use std::sync::atomic::{AtomicBool, Ordering};
99

1010
use std::thread;
@@ -176,21 +176,27 @@ fn dev_frontend(staging: bool) {
176176
/// Perform all backend setup: postgres, database, env files, migrations, and
177177
/// TypeScript binding generation.
178178
fn setup_backend(repo_root: &Path) {
179-
// Try to load env files so DATABASE_URL is available. This is best-effort;
180-
// if .env.development doesn't exist and .env doesn't exist either, we may
181-
// prompt the user below.
182-
load_env_files_if_available(repo_root);
183-
184-
// If DATABASE_URL is still not set and postgres is already running, ask the
185-
// user for the connection URL and write it to .env.
186-
if env::var("DATABASE_URL").is_err() && pg_is_ready_at("localhost", "5432") {
187-
let url = prompt_database_url();
188-
write_env_database_url(repo_root, &url);
179+
let has_env = env_files_exist(repo_root);
180+
181+
if !has_env && pg_is_ready_at("localhost", "5432") {
182+
// Postgres is already running but there are no .env files.
183+
// Prompt the user for connection details and write .env.
184+
let defaults = default_pg_config(repo_root);
185+
let pg = prompt_pg_config(&defaults);
186+
let url = pg.to_url();
187+
write_env_files(repo_root, &url);
188+
// Load the written .env so child processes inherit all variables.
189+
load_env_file(&repo_root.join("packages").join("backend").join(".env"));
190+
ensure_database_and_user(&pg);
191+
} else {
192+
// Either .env already exists, or postgres is not running and we'll
193+
// manage it ourselves with the defaults from .env.development.
194+
ensure_env_files(repo_root);
195+
let pg = pg_config_from_env();
196+
ensure_postgres_running(&pg);
197+
ensure_database_and_user(&pg);
189198
}
190199

191-
let pg = pg_config_from_env();
192-
ensure_postgres_running(&pg);
193-
ensure_database_and_user(&pg);
194200
run_migrations(repo_root);
195201
generate_bindings(repo_root);
196202
}
@@ -403,6 +409,16 @@ struct PgConfig {
403409
dbname: String,
404410
}
405411

412+
impl PgConfig {
413+
/// Reconstruct a `postgres://` connection URL from the components.
414+
fn to_url(&self) -> String {
415+
format!(
416+
"postgres://{}:{}@{}:{}/{}",
417+
self.user, self.password, self.host, self.port, self.dbname
418+
)
419+
}
420+
}
421+
406422
/// Parse `DATABASE_URL` from the environment into a `PgConfig`.
407423
///
408424
/// Expects the standard libpq format: `postgres://user:password@host:port/dbname`.
@@ -619,7 +635,9 @@ fn ensure_database_and_user(pg: &PgConfig) {
619635
// Ensure the user owns the database and has schema permissions.
620636
let grant_sql = format!("GRANT ALL ON SCHEMA public TO {};", pg.user);
621637
if !try_run_psql(pg, &pg.dbname.clone(), &grant_sql) {
622-
eprintln!("[catcom] Warning: could not grant schema permissions (this is fine if already granted).");
638+
eprintln!(
639+
"[catcom] Warning: could not grant schema permissions (this is fine if already granted)."
640+
);
623641
}
624642

625643
println!("[catcom] Database ready.");
@@ -645,47 +663,66 @@ fn try_run_psql(pg: &PgConfig, database: &str, sql: &str) -> bool {
645663
// Environment files
646664
// ---------------------------------------------------------------------------
647665

648-
/// Try to load `.env` files if they are available.
649-
///
650-
/// If `.env.development` exists, copies it to `.env` targets that don't
651-
/// already exist (the original behaviour). If `.env` files already exist,
652-
/// loads them. If neither exists, this is a no-op — the caller is expected
653-
/// to handle the missing `DATABASE_URL` (e.g. by prompting the user).
654-
fn load_env_files_if_available(repo_root: &Path) {
655-
let backend_env = repo_root.join("packages").join("backend").join(".env");
656-
let migrator_env = repo_root.join("packages").join("migrator").join(".env");
666+
/// Check whether `.env` files already exist for the backend and migrator.
667+
fn env_files_exist(repo_root: &Path) -> bool {
668+
repo_root.join("packages").join("backend").join(".env").exists()
669+
&& repo_root.join("packages").join("migrator").join(".env").exists()
670+
}
671+
672+
/// Read the default PgConfig from `.env.development`, falling back to
673+
/// hard-coded defaults if the file doesn't exist.
674+
fn default_pg_config(repo_root: &Path) -> PgConfig {
657675
let source = repo_root.join("packages").join("backend").join(".env.development");
676+
if source.exists()
677+
&& let Some(url) = read_env_var_from_file(&source, "DATABASE_URL")
678+
{
679+
return parse_database_url(&url);
680+
}
681+
PgConfig {
682+
host: "localhost".to_string(),
683+
port: "5432".to_string(),
684+
user: "catcolab".to_string(),
685+
password: "password".to_string(),
686+
dbname: "catcolab".to_string(),
687+
}
688+
}
658689

659-
let targets = [&backend_env, &migrator_env];
660-
661-
// If .env.development exists, copy it to any missing targets.
662-
if source.exists() {
663-
for target in &targets {
664-
if !target.exists() {
665-
println!(
666-
"[catcom] Copying .env.development -> {}",
667-
target.strip_prefix(repo_root).unwrap_or(target).display()
668-
);
669-
fs::copy(&source, target).unwrap_or_else(|e| {
670-
eprintln!("Error: failed to copy .env file: {e}");
671-
exit(1);
672-
});
673-
}
690+
/// Read a single variable value from a .env file.
691+
fn read_env_var_from_file(path: &Path, key: &str) -> Option<String> {
692+
let contents = fs::read_to_string(path).ok()?;
693+
for line in contents.lines() {
694+
let line = line.trim();
695+
if line.is_empty() || line.starts_with('#') {
696+
continue;
697+
}
698+
if let Some((k, v)) = line.split_once('=')
699+
&& k.trim() == key
700+
{
701+
return Some(v.trim().to_string());
674702
}
675703
}
704+
None
705+
}
676706

677-
// Load variables from whichever env file exists.
678-
if backend_env.exists() {
679-
load_env_file(&backend_env);
680-
} else if source.exists() {
681-
load_env_file(&source);
682-
}
707+
/// Prompt the user for each PG connection field, showing a default in brackets.
708+
/// Pressing enter accepts the default.
709+
fn prompt_pg_config(defaults: &PgConfig) -> PgConfig {
710+
println!("[catcom] PostgreSQL is running but no .env files found.");
711+
println!("[catcom] Enter connection details (press Enter to accept defaults):");
712+
713+
let user = prompt_field(" User", &defaults.user);
714+
let password = prompt_field(" Password", &defaults.password);
715+
let host = prompt_field(" Host", &defaults.host);
716+
let port = prompt_field(" Port", &defaults.port);
717+
let dbname = prompt_field(" Database", &defaults.dbname);
718+
719+
PgConfig { host, port, user, password, dbname }
683720
}
684721

685-
/// Prompt the user for a DATABASE_URL on stdin and return the entered value.
686-
fn prompt_database_url() -> String {
687-
println!("[catcom] PostgreSQL is running but no DATABASE_URL is configured.");
688-
print!("[catcom] Enter DATABASE_URL (e.g. postgres://user:password@localhost:5432/dbname): ");
722+
/// Prompt for a single field with a default value. Returns the default if
723+
/// the user presses Enter without typing anything.
724+
fn prompt_field(label: &str, default: &str) -> String {
725+
print!("{label} [{default}]: ");
689726
io::stdout().flush().unwrap();
690727

691728
let mut line = String::new();
@@ -694,54 +731,88 @@ fn prompt_database_url() -> String {
694731
exit(1);
695732
});
696733

697-
let url = line.trim().to_string();
698-
if url.is_empty() {
699-
eprintln!("Error: no DATABASE_URL provided.");
700-
exit(1);
734+
let value = line.trim();
735+
if value.is_empty() {
736+
default.to_string()
737+
} else {
738+
value.to_string()
701739
}
702-
703-
url
704740
}
705741

706-
/// Write `DATABASE_URL` to `.env` files in `packages/backend/` and
707-
/// `packages/migrator/`, and set it in the current process environment.
708-
fn write_env_database_url(repo_root: &Path, url: &str) {
742+
/// Write `.env` files for the backend and migrator with the given DATABASE_URL
743+
/// (plus FIREBASE_PROJECT_ID from `.env.development` if available).
744+
fn write_env_files(repo_root: &Path, database_url: &str) {
745+
let source = repo_root.join("packages").join("backend").join(".env.development");
746+
747+
// Start with DATABASE_URL, then carry over any other variables from
748+
// .env.development (e.g. FIREBASE_PROJECT_ID).
749+
let mut contents = format!("DATABASE_URL={database_url}\n");
750+
if source.exists()
751+
&& let Ok(src) = fs::read_to_string(&source)
752+
{
753+
for line in src.lines() {
754+
let trimmed = line.trim();
755+
if trimmed.is_empty() || trimmed.starts_with('#') {
756+
contents.push_str(line);
757+
contents.push('\n');
758+
continue;
759+
}
760+
// Skip DATABASE_URL since we already wrote it.
761+
if let Some((k, _)) = trimmed.split_once('=')
762+
&& k.trim() != "DATABASE_URL"
763+
{
764+
contents.push_str(line);
765+
contents.push('\n');
766+
}
767+
}
768+
}
769+
709770
let targets = [
710771
repo_root.join("packages").join("backend").join(".env"),
711772
repo_root.join("packages").join("migrator").join(".env"),
712773
];
713774

714-
let line = format!("DATABASE_URL={url}\n");
715-
716775
for target in &targets {
717776
let display_path = target.strip_prefix(repo_root).unwrap_or(target);
777+
println!("[catcom] Writing {}", display_path.display());
778+
fs::write(target, &contents).unwrap_or_else(|e| {
779+
eprintln!("Error: failed to write {}: {e}", display_path.display());
780+
exit(1);
781+
});
782+
}
783+
}
718784

719-
if target.exists() {
720-
// .env exists but doesn't contain DATABASE_URL — append it.
721-
let contents = fs::read_to_string(target).unwrap_or_default();
722-
if !contents.lines().any(|l| l.trim().starts_with("DATABASE_URL=")) {
723-
println!("[catcom] Appending DATABASE_URL to {}", display_path.display());
724-
let mut file =
725-
fs::OpenOptions::new().append(true).open(target).unwrap_or_else(|e| {
726-
eprintln!("Error: failed to open {}: {e}", display_path.display());
727-
exit(1);
728-
});
729-
file.write_all(line.as_bytes()).unwrap_or_else(|e| {
730-
eprintln!("Error: failed to write to {}: {e}", display_path.display());
731-
exit(1);
732-
});
733-
}
734-
} else {
735-
println!("[catcom] Writing DATABASE_URL to {}", display_path.display());
736-
fs::write(target, &line).unwrap_or_else(|e| {
737-
eprintln!("Error: failed to write {}: {e}", display_path.display());
785+
/// Ensure `.env` files exist in `packages/backend/` and `packages/migrator/`,
786+
/// and load environment variables from the env file into the current process
787+
/// so that child processes (migrator, backend) inherit them.
788+
fn ensure_env_files(repo_root: &Path) {
789+
let source = repo_root.join("packages").join("backend").join(".env.development");
790+
791+
if !source.exists() {
792+
eprintln!("Error: {} not found.", source.display());
793+
exit(1);
794+
}
795+
796+
let targets = [
797+
repo_root.join("packages").join("backend").join(".env"),
798+
repo_root.join("packages").join("migrator").join(".env"),
799+
];
800+
801+
for target in &targets {
802+
if !target.exists() {
803+
println!(
804+
"[catcom] Copying .env.development -> {}",
805+
target.strip_prefix(repo_root).unwrap_or(target).display()
806+
);
807+
fs::copy(&source, target).unwrap_or_else(|e| {
808+
eprintln!("Error: failed to copy .env file: {e}");
738809
exit(1);
739810
});
740811
}
741812
}
742813

743-
// SAFETY: called from main thread before spawning child processes.
744-
unsafe { env::set_var("DATABASE_URL", url) };
814+
// Load variables from the env file so child processes inherit them.
815+
load_env_file(&source);
745816
}
746817

747818
/// Parse a .env file and set any variables not already present in the

0 commit comments

Comments
 (0)