@@ -18,6 +18,7 @@ import io.micronaut.core.annotation.ReflectiveAccess
1818import picocli.CommandLine.Command
1919import picocli.CommandLine.Option
2020import picocli.CommandLine.Parameters
21+ import java.net.Socket
2122import java.nio.file.Files
2223import java.nio.file.Path
2324import java.nio.file.StandardCopyOption
@@ -30,12 +31,18 @@ import kotlin.io.path.listDirectoryEntries
3031import kotlin.io.path.name
3132import kotlin.io.path.readText
3233import kotlin.io.path.writeText
34+ import kotlinx.coroutines.async
35+ import kotlinx.coroutines.awaitAll
36+ import kotlinx.coroutines.coroutineScope
37+ import kotlinx.coroutines.delay
3338import kotlinx.serialization.Serializable
3439import kotlinx.serialization.json.Json
3540import elide.tool.cli.AbstractSubcommand
3641import elide.tool.cli.CommandContext
3742import elide.tool.cli.CommandResult
3843import elide.tool.cli.ToolState
44+ import elide.tool.exec.SubprocessRunner.runTask
45+ import elide.tool.exec.SubprocessRunner.stringToTask
3946
4047@Command(
4148 name = " db" ,
@@ -74,8 +81,26 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
7481 private const val STUDIO_OUTPUT_DIR = " .dev/db-studio"
7582 private const val STUDIO_INDEX_FILE = " index.ts"
7683 private val SQLITE_EXTENSIONS = setOf (" .db" , " .sqlite" , " .sqlite3" , " .db3" )
84+ private const val MAX_WAIT_SECONDS = 60
85+ private const val PORT_CHECK_INTERVAL_MS = 1000L
7786 }
7887
88+ private suspend fun waitForServerReady (port : Int , name : String , maxWait : Int = MAX_WAIT_SECONDS ): Boolean {
89+ var waited = 0
90+ while (waited < maxWait) {
91+ if (isPortListening(port)) {
92+ return true
93+ }
94+ delay(PORT_CHECK_INTERVAL_MS )
95+ waited++
96+ }
97+ return false
98+ }
99+
100+ private fun isPortListening (port : Int ): Boolean = runCatching {
101+ Socket (" localhost" , port).use { true }
102+ }.getOrDefault(false )
103+
79104 // Extension function for SQLite detection
80105 private fun Path.isSqliteDatabase (): Boolean =
81106 isRegularFile() && SQLITE_EXTENSIONS .any { name.endsWith(it, ignoreCase = true ) }
@@ -286,24 +311,59 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
286311 configFile.writeText(configContent)
287312
288313 output {
289- appendLine(" Database Studio files generated in: ${outputDir.toAbsolutePath()} " )
290- appendLine()
291- appendLine(" To start the Database Studio:" )
292- appendLine()
293- appendLine(" Terminal 1 (API Server on port $apiPort ):" )
294- appendLine(" cd ${apiDir.toAbsolutePath()} " )
295- appendLine(" elide serve" )
296- appendLine()
297- appendLine(" Terminal 2 (UI Server on port $port ):" )
298- appendLine(" cd ${uiDir.toAbsolutePath()} " )
299- appendLine(" elide serve" )
300- appendLine()
301- appendLine(" Then open: http://localhost:$port " )
302- appendLine()
303- appendLine(" Note: Port configuration is read from elide.pkl in each directory" )
314+ appendLine(" Starting Database Studio..." )
304315 appendLine()
305316 }
306317
307- return CommandResult .success()
318+ // Start both servers concurrently and wait for them
319+ return try {
320+ coroutineScope {
321+ // Start API server task
322+ val apiTask = async {
323+ runTask(stringToTask(
324+ " elide run $STUDIO_INDEX_FILE " ,
325+ shell = elide.tooling.runner.ProcessRunner .ProcessShell .None ,
326+ workingDirectory = apiDir
327+ ))
328+ }
329+
330+ // Start UI server task
331+ val uiTask = async {
332+ runTask(stringToTask(
333+ " elide serve $STUDIO_OUTPUT_DIR /ui" ,
334+ shell = elide.tooling.runner.ProcessRunner .ProcessShell .None
335+ ))
336+ }
337+
338+ // Wait for API server to be ready
339+ if (! waitForServerReady(apiPort, " API Server" )) {
340+ return @coroutineScope CommandResult .err(message = " API Server didn't start within $MAX_WAIT_SECONDS seconds" )
341+ }
342+
343+ // Wait for UI server to be ready
344+ if (! waitForServerReady(port, " UI Server" )) {
345+ return @coroutineScope CommandResult .err(message = " UI Server didn't start within $MAX_WAIT_SECONDS seconds" )
346+ }
347+
348+ output {
349+ appendLine()
350+ appendLine(" ✓ Database Studio is running!" )
351+ appendLine()
352+ appendLine(" UI: http://localhost:$port " )
353+ appendLine(" API: http://localhost:$apiPort " )
354+ appendLine()
355+ appendLine(" Press Ctrl+C to stop all servers" )
356+ appendLine()
357+ }
358+
359+ // Wait for both servers to complete (they run until interrupted)
360+ awaitAll(apiTask.await().asDeferred(), uiTask.await().asDeferred())
361+
362+ CommandResult .success()
363+ }
364+ } catch (e: Exception ) {
365+ // Servers were interrupted or failed, clean up gracefully
366+ CommandResult .success()
367+ }
308368 }
309369}
0 commit comments