@@ -310,54 +310,71 @@ async function executepostSetup(projectDir: string, commands: PostSetupCommand[]
310
310
// Helper: wait for PostgreSQL if project uses it
311
311
async function waitForPostgresIfNeeded ( ) : Promise < void > {
312
312
try {
313
- const { parseEnvFile } = await import ( '../dev-setup' )
314
- const env = parseEnvFile ( path . join ( projectDir , '.env' ) )
315
- const usesPg = ( env . DB_CONNECTION || '' ) . toLowerCase ( ) . includes ( 'pg' )
316
- if ( ! usesPg )
317
- return
318
-
319
- const host = env . DB_HOST || '127.0.0.1'
320
- const port = env . DB_PORT || '5432'
313
+ let host = '127.0.0.1'
314
+ let port = '5432'
315
+ try {
316
+ const { parseEnvFile } = await import ( '../dev-setup' )
317
+ const env = parseEnvFile ( path . join ( projectDir , '.env' ) )
318
+ host = env . DB_HOST || host
319
+ port = env . DB_PORT || port
320
+ }
321
+ catch { }
321
322
322
- const pgIsReady = ( await import ( '../utils' ) ) . findBinaryInPath ( 'pg_isready' ) || 'pg_isready'
323
- // Probe up to 15 times with backoff
324
- for ( let i = 0 ; i < 15 ; i ++ ) {
323
+ const { findBinaryInPath } = await import ( '../utils' )
324
+ const pgIsReady = findBinaryInPath ( 'pg_isready' ) || 'pg_isready'
325
+ let ready = false
326
+ // First pass: pg_isready
327
+ for ( let i = 0 ; i < 20 && ! ready ; i ++ ) {
325
328
const ok = await new Promise < boolean > ( ( resolve ) => {
326
329
// eslint-disable-next-line ts/no-require-imports
327
330
const { spawn } = require ( 'node:child_process' )
328
- const p = spawn ( pgIsReady , [ '-h' , host , '-p' , port ] , { stdio : 'pipe' } )
331
+ const p = spawn ( pgIsReady , [ '-h' , host , '-p' , String ( port ) ] , { stdio : 'pipe' } )
329
332
p . on ( 'close' , ( code : number ) => resolve ( code === 0 ) )
330
333
p . on ( 'error' , ( ) => resolve ( false ) )
331
334
} )
332
- if ( ok )
333
- break
334
- await new Promise ( r => setTimeout ( r , Math . min ( 500 + i * 500 , 5000 ) ) )
335
+ ready = ok
336
+ if ( ! ready )
337
+ await new Promise ( r => setTimeout ( r , 250 + i * 150 ) )
338
+ }
339
+ // Fallback: TCP probe
340
+ if ( ! ready ) {
341
+ const net = await import ( 'node:net' )
342
+ for ( let i = 0 ; i < 20 && ! ready ; i ++ ) {
343
+ const ok = await new Promise < boolean > ( ( resolve ) => {
344
+ const socket = net . connect ( { host, port : Number ( port ) , timeout : 1000 } , ( ) => {
345
+ socket . end ( )
346
+ resolve ( true )
347
+ } )
348
+ socket . on ( 'error' , ( ) => resolve ( false ) )
349
+ socket . on ( 'timeout' , ( ) => {
350
+ socket . destroy ( )
351
+ resolve ( false )
352
+ } )
353
+ } )
354
+ ready = ok
355
+ if ( ! ready )
356
+ await new Promise ( r => setTimeout ( r , 250 + i * 150 ) )
357
+ }
335
358
}
336
359
// Small grace delay after ready
337
- await new Promise ( r => setTimeout ( r , 250 ) )
338
-
339
- // If still not listening, wait a bit longer with additional probes instead of restarting
340
- const readyAfterFirstPass = await new Promise < boolean > ( ( resolve ) => {
341
- // eslint-disable-next-line ts/no-require-imports
342
- const { spawn } = require ( 'node:child_process' )
343
- const p = spawn ( pgIsReady , [ '-h' , host , '-p' , port ] , { stdio : 'ignore' } )
344
- p . on ( 'close' , ( code : number ) => resolve ( code === 0 ) )
345
- p . on ( 'error' , ( ) => resolve ( false ) )
346
- } )
347
- if ( ! readyAfterFirstPass ) {
348
- for ( let i = 0 ; i < 12 ; i ++ ) {
360
+ if ( ready )
361
+ await new Promise ( r => setTimeout ( r , 250 ) )
362
+
363
+ // Final verification using psql simple query if available
364
+ const psqlBin = findBinaryInPath ( 'psql' ) || 'psql'
365
+ if ( psqlBin ) {
366
+ for ( let i = 0 ; i < 8 ; i ++ ) {
349
367
const ok = await new Promise < boolean > ( ( resolve ) => {
350
368
// eslint-disable-next-line ts/no-require-imports
351
369
const { spawn } = require ( 'node:child_process' )
352
- const p = spawn ( pgIsReady , [ '-h' , host , '-p' , port ] , { stdio : 'ignore' } )
370
+ const p = spawn ( psqlBin , [ '-h' , host , '-p' , String ( port ) , '-U' , 'postgres' , '-tAc' , 'SELECT 1' ] , { stdio : 'ignore' } )
353
371
p . on ( 'close' , ( code : number ) => resolve ( code === 0 ) )
354
372
p . on ( 'error' , ( ) => resolve ( false ) )
355
373
} )
356
374
if ( ok )
357
375
break
358
- await new Promise ( r => setTimeout ( r , Math . min ( 750 + i * 250 , 3000 ) ) )
376
+ await new Promise ( r => setTimeout ( r , 300 + i * 200 ) )
359
377
}
360
- await new Promise ( r => setTimeout ( r , 250 ) )
361
378
}
362
379
}
363
380
catch { }
@@ -391,7 +408,27 @@ async function executepostSetup(projectDir: string, commands: PostSetupCommand[]
391
408
. filter ( Boolean )
392
409
. join ( ':' )
393
410
394
- const execEnv = { ...process . env , PATH : composedPath }
411
+ const execEnv : Record < string , string > = { ...process . env , PATH : composedPath }
412
+ // Ensure DB defaults for tools that honor process/env
413
+ if ( ! execEnv . DB_HOST )
414
+ execEnv . DB_HOST = '127.0.0.1'
415
+ if ( ! execEnv . DB_PORT )
416
+ execEnv . DB_PORT = '5432'
417
+ if ( ! execEnv . DB_USERNAME && ! execEnv . DB_USER )
418
+ execEnv . DB_USERNAME = 'root'
419
+ if ( ! execEnv . DB_PASSWORD )
420
+ execEnv . DB_PASSWORD = ''
421
+ // Provide DATABASE_URL to override framework defaults when supported
422
+ try {
423
+ const dbName = path . basename ( projectDir ) . replace ( / \W / g, '_' )
424
+ if ( ! execEnv . DATABASE_URL ) {
425
+ const u = execEnv . DB_USERNAME || 'root'
426
+ const p = execEnv . DB_PASSWORD || ''
427
+ const cred = p ? `${ u } :${ p } ` : `${ u } `
428
+ execEnv . DATABASE_URL = `pgsql://${ cred } @${ execEnv . DB_HOST } :${ execEnv . DB_PORT } /${ dbName } `
429
+ }
430
+ }
431
+ catch { }
395
432
396
433
if ( command . runInBackground ) {
397
434
execSync ( command . command , {
@@ -703,6 +740,8 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
703
740
await ensureProjectPhpIni ( projectDir , envDir )
704
741
try {
705
742
await setupProjectServices ( projectDir , sniffResult , true )
743
+ // Also run post-setup once in shell fast path (idempotent marker)
744
+ await maybeRunProjectPostSetup ( projectDir , envDir , true )
706
745
}
707
746
catch { }
708
747
}
@@ -896,6 +935,12 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
896
935
// Ensure project php.ini exists only
897
936
await ensureProjectPhpIni ( projectDir , envDir )
898
937
938
+ // Run project-level post-setup once (idempotent marker)
939
+ try {
940
+ await maybeRunProjectPostSetup ( projectDir , envDir , true )
941
+ }
942
+ catch { }
943
+
899
944
outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
900
945
return
901
946
}
@@ -943,7 +988,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
943
988
// Ensure php.ini and Laravel post-setup runs (regular path)
944
989
await ensureProjectPhpIni ( projectDir , envDir )
945
990
if ( ! isShellIntegration ) {
946
- await maybeRunLaravelPostSetup ( projectDir , envDir , isShellIntegration )
991
+ await maybeRunProjectPostSetup ( projectDir , envDir , isShellIntegration )
947
992
}
948
993
949
994
// Mark environment as ready for fast shell activation on subsequent prompts
@@ -1450,7 +1495,7 @@ async function createPhpShimsAfterInstall(envDir: string): Promise<void> {
1450
1495
/**
1451
1496
* Run Laravel post-setup commands once per project activation
1452
1497
*/
1453
- async function maybeRunLaravelPostSetup ( projectDir : string , envDir : string , _isShellIntegration : boolean ) : Promise < void > {
1498
+ async function maybeRunProjectPostSetup ( projectDir : string , envDir : string , _isShellIntegration : boolean ) : Promise < void > {
1454
1499
try {
1455
1500
const projectPostSetup = config . postSetup
1456
1501
if ( ! projectPostSetup ?. enabled ) {
@@ -1510,7 +1555,6 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
1510
1555
1511
1556
// Import service manager
1512
1557
const { startService } = await import ( '../services/manager' )
1513
- const { createProjectDatabase } = await import ( '../services/database' )
1514
1558
1515
1559
// Start each service specified in autoStart
1516
1560
for ( const serviceName of autoStartServices ) {
@@ -1528,63 +1572,8 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
1528
1572
1529
1573
// Special handling for PostgreSQL: wait for readiness and create project database
1530
1574
if ( ( serviceName === 'postgres' || serviceName === 'postgresql' ) && hasPostgresInDeps ) {
1531
- if ( showMessages )
1532
- console . log ( '⏳ Verifying PostgreSQL readiness...' )
1533
- // Ensure postgres is actually accepting connections
1534
- try {
1535
- const { findBinaryInPath } = await import ( '../utils' )
1536
- const pgIsReady = findBinaryInPath ( 'pg_isready' ) || 'pg_isready'
1537
- // Probe up to 10 times
1538
- for ( let i = 0 ; i < 10 ; i ++ ) {
1539
- const probe = await new Promise < boolean > ( ( resolve ) => {
1540
- // eslint-disable-next-line ts/no-require-imports
1541
- const { spawn } = require ( 'node:child_process' )
1542
- const p = spawn ( pgIsReady , [ '-h' , '127.0.0.1' , '-p' , '5432' ] , { stdio : 'pipe' } )
1543
- p . on ( 'close' , ( code : number ) => resolve ( code === 0 ) )
1544
- p . on ( 'error' , ( ) => resolve ( false ) )
1545
- } )
1546
- if ( probe )
1547
- break
1548
- await new Promise ( r => setTimeout ( r , Math . min ( 1000 * ( i + 1 ) , 5000 ) ) )
1549
- }
1550
- }
1551
- catch { }
1552
-
1553
- if ( showMessages )
1554
- console . log ( '🔧 Creating project PostgreSQL database...' )
1555
- const projectName = path . basename ( projectDir ) . replace ( / \W / g, '_' )
1556
- try {
1557
- // Ensure DB utilities resolve from project environment first
1558
- const projectHash = generateProjectHash ( projectDir )
1559
- const envDir = path . join ( process . env . HOME || '' , '.local' , 'share' , 'launchpad' , 'envs' , projectHash )
1560
- const globalEnvDir = path . join ( process . env . HOME || '' , '.local' , 'share' , 'launchpad' , 'global' )
1561
- const envBinPath = path . join ( envDir , 'bin' )
1562
- const envSbinPath = path . join ( envDir , 'sbin' )
1563
- const globalBinPath = path . join ( globalEnvDir , 'bin' )
1564
- const globalSbinPath = path . join ( globalEnvDir , 'sbin' )
1565
- const originalPath = process . env . PATH || ''
1566
- const augmentedPath = [ envBinPath , envSbinPath , globalBinPath , globalSbinPath , originalPath ]
1567
- . filter ( Boolean )
1568
- . join ( ':' )
1569
- process . env . PATH = augmentedPath
1570
-
1571
- await createProjectDatabase ( projectName , {
1572
- type : 'postgres' ,
1573
- host : '127.0.0.1' ,
1574
- port : 5432 ,
1575
- user : 'postgres' ,
1576
- password : '' ,
1577
- } )
1578
-
1579
- if ( showMessages ) {
1580
- console . log ( `✅ PostgreSQL database '${ projectName } ' created` )
1581
- }
1582
- }
1583
- catch ( dbError ) {
1584
- if ( showMessages ) {
1585
- console . warn ( `⚠️ Database creation warning: ${ dbError instanceof Error ? dbError . message : String ( dbError ) } ` )
1586
- }
1587
- }
1575
+ // Manager has already waited and verified health before returning true.
1576
+ process . env . LAUNCHPAD_PG_READY = '1'
1588
1577
}
1589
1578
}
1590
1579
else if ( showMessages ) {
@@ -1598,9 +1587,7 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
1598
1587
}
1599
1588
}
1600
1589
}
1601
- catch ( error ) {
1602
- if ( showMessages ) {
1603
- console . warn ( `⚠️ Service setup warning: ${ error instanceof Error ? error . message : String ( error ) } ` )
1604
- }
1590
+ catch {
1591
+ // non-fatal
1605
1592
}
1606
1593
}
0 commit comments