Skip to content

Commit 1f87d46

Browse files
committed
chore: wip
1 parent 26ca5f6 commit 1f87d46

File tree

3 files changed

+223
-120
lines changed

3 files changed

+223
-120
lines changed

packages/launchpad/src/dev/dump.ts

Lines changed: 82 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -310,54 +310,71 @@ async function executepostSetup(projectDir: string, commands: PostSetupCommand[]
310310
// Helper: wait for PostgreSQL if project uses it
311311
async function waitForPostgresIfNeeded(): Promise<void> {
312312
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 {}
321322

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++) {
325328
const ok = await new Promise<boolean>((resolve) => {
326329
// eslint-disable-next-line ts/no-require-imports
327330
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' })
329332
p.on('close', (code: number) => resolve(code === 0))
330333
p.on('error', () => resolve(false))
331334
})
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+
}
335358
}
336359
// 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++) {
349367
const ok = await new Promise<boolean>((resolve) => {
350368
// eslint-disable-next-line ts/no-require-imports
351369
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' })
353371
p.on('close', (code: number) => resolve(code === 0))
354372
p.on('error', () => resolve(false))
355373
})
356374
if (ok)
357375
break
358-
await new Promise(r => setTimeout(r, Math.min(750 + i * 250, 3000)))
376+
await new Promise(r => setTimeout(r, 300 + i * 200))
359377
}
360-
await new Promise(r => setTimeout(r, 250))
361378
}
362379
}
363380
catch {}
@@ -391,7 +408,27 @@ async function executepostSetup(projectDir: string, commands: PostSetupCommand[]
391408
.filter(Boolean)
392409
.join(':')
393410

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 {}
395432

396433
if (command.runInBackground) {
397434
execSync(command.command, {
@@ -703,6 +740,8 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
703740
await ensureProjectPhpIni(projectDir, envDir)
704741
try {
705742
await setupProjectServices(projectDir, sniffResult, true)
743+
// Also run post-setup once in shell fast path (idempotent marker)
744+
await maybeRunProjectPostSetup(projectDir, envDir, true)
706745
}
707746
catch {}
708747
}
@@ -896,6 +935,12 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
896935
// Ensure project php.ini exists only
897936
await ensureProjectPhpIni(projectDir, envDir)
898937

938+
// Run project-level post-setup once (idempotent marker)
939+
try {
940+
await maybeRunProjectPostSetup(projectDir, envDir, true)
941+
}
942+
catch {}
943+
899944
outputShellCode(dir, envBinPath, envSbinPath, projectHash, sniffResult, globalBinPath, globalSbinPath)
900945
return
901946
}
@@ -943,7 +988,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
943988
// Ensure php.ini and Laravel post-setup runs (regular path)
944989
await ensureProjectPhpIni(projectDir, envDir)
945990
if (!isShellIntegration) {
946-
await maybeRunLaravelPostSetup(projectDir, envDir, isShellIntegration)
991+
await maybeRunProjectPostSetup(projectDir, envDir, isShellIntegration)
947992
}
948993

949994
// Mark environment as ready for fast shell activation on subsequent prompts
@@ -1450,7 +1495,7 @@ async function createPhpShimsAfterInstall(envDir: string): Promise<void> {
14501495
/**
14511496
* Run Laravel post-setup commands once per project activation
14521497
*/
1453-
async function maybeRunLaravelPostSetup(projectDir: string, envDir: string, _isShellIntegration: boolean): Promise<void> {
1498+
async function maybeRunProjectPostSetup(projectDir: string, envDir: string, _isShellIntegration: boolean): Promise<void> {
14541499
try {
14551500
const projectPostSetup = config.postSetup
14561501
if (!projectPostSetup?.enabled) {
@@ -1510,7 +1555,6 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
15101555

15111556
// Import service manager
15121557
const { startService } = await import('../services/manager')
1513-
const { createProjectDatabase } = await import('../services/database')
15141558

15151559
// Start each service specified in autoStart
15161560
for (const serviceName of autoStartServices) {
@@ -1528,63 +1572,8 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
15281572

15291573
// Special handling for PostgreSQL: wait for readiness and create project database
15301574
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'
15881577
}
15891578
}
15901579
else if (showMessages) {
@@ -1598,9 +1587,7 @@ async function setupProjectServices(projectDir: string, sniffResult: any, showMe
15981587
}
15991588
}
16001589
}
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
16051592
}
16061593
}

packages/launchpad/src/services/database.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,36 +54,44 @@ async function createPostgreSQLDatabase(dbName: string, options: DatabaseOptions
5454
const { host = '127.0.0.1', port = 5432, user = 'postgres' } = options
5555

5656
try {
57-
// Ensure server is accepting TCP connections before attempting to create DB
57+
// Ensure server is accepting connections before attempting to create DB
5858
const maxAttempts = 20
59-
let ready = false
60-
for (let i = 0; i < maxAttempts; i++) {
61-
const attempt = await new Promise<boolean>((resolve) => {
62-
const socket = net.connect({ host, port, timeout: 1000 }, () => {
63-
socket.end()
64-
resolve(true)
65-
})
66-
socket.on('error', () => resolve(false))
67-
socket.on('timeout', () => {
68-
socket.destroy()
69-
resolve(false)
70-
})
71-
})
72-
if (attempt) {
73-
ready = true
74-
break
75-
}
76-
await new Promise(r => setTimeout(r, 250 + i * 150))
77-
}
78-
if (!ready) {
79-
// As a fallback, try pg_isready if available
80-
const pgIsReady = findBinaryInPath('pg_isready') || 'pg_isready'
59+
let ready = process.env.LAUNCHPAD_PG_READY === '1'
60+
const pgIsReadyBin = findBinaryInPath('pg_isready') || 'pg_isready'
61+
62+
for (let i = 0; i < maxAttempts && !ready; i++) {
63+
let okPg = false
8164
try {
82-
await executeCommand([pgIsReady, '-h', host, '-p', String(port)])
83-
ready = true
65+
await executeCommand([pgIsReadyBin, '-h', host, '-p', String(port)])
66+
okPg = true
8467
}
8568
catch {}
69+
70+
let okTcp = false
71+
if (!okPg) {
72+
okTcp = await new Promise<boolean>((resolve) => {
73+
const socket = net.connect({ host, port, timeout: 1000 }, () => {
74+
socket.end()
75+
resolve(true)
76+
})
77+
socket.on('error', () => resolve(false))
78+
socket.on('timeout', () => {
79+
socket.destroy()
80+
resolve(false)
81+
})
82+
})
83+
}
84+
85+
ready = okPg || okTcp
86+
87+
if (!ready) {
88+
if (process.env.LAUNCHPAD_DEBUG === '1') {
89+
console.warn(`⏳ PostgreSQL readiness attempt ${i + 1}/${maxAttempts} not ready; methods: pg_isready=${okPg ? 'ok' : 'fail'} tcp=${okTcp ? 'ok' : 'fail'}`)
90+
}
91+
await new Promise(r => setTimeout(r, 250 + i * 150))
92+
}
8693
}
94+
8795
if (!ready)
8896
throw new Error('PostgreSQL not accepting connections yet')
8997
// Small grace period after accept

0 commit comments

Comments
 (0)