@@ -4,7 +4,7 @@ use std::env;
44use std:: fs;
55use std:: io:: { self , BufRead , Write as _} ;
66use std:: path:: { Path , PathBuf } ;
7- use std:: process:: { exit , Child , Command , Stdio } ;
7+ use std:: process:: { Child , Command , Stdio , exit } ;
88use std:: sync:: atomic:: { AtomicBool , Ordering } ;
99
1010use 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.
178178fn 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