@@ -18,11 +18,9 @@ import io.micronaut.core.annotation.ReflectiveAccess
1818import picocli.CommandLine.Command
1919import picocli.CommandLine.Option
2020import picocli.CommandLine.Parameters
21- import java.nio.file.FileSystems
2221import java.nio.file.Files
2322import java.nio.file.Path
2423import java.nio.file.StandardCopyOption
25- import java.nio.file.attribute.BasicFileAttributes
2624import kotlin.io.path.exists
2725import kotlin.io.path.fileSize
2826import kotlin.io.path.getLastModifiedTime
@@ -33,7 +31,6 @@ import kotlin.io.path.name
3331import kotlin.io.path.readText
3432import kotlin.io.path.writeText
3533import kotlinx.serialization.Serializable
36- import kotlinx.serialization.encodeToString
3734import kotlinx.serialization.json.Json
3835import elide.tool.cli.AbstractSubcommand
3936import elide.tool.cli.CommandContext
@@ -73,7 +70,8 @@ data class DiscoveredDatabase(
7370internal class DbStudioCommand : AbstractSubcommand <ToolState , CommandContext >() {
7471
7572 private companion object {
76- private const val STUDIO_RESOURCE_PATH = " db-studio"
73+ private const val STUDIO_API_SOURCE = " samples/db-studio/api"
74+ private const val STUDIO_UI_SOURCE = " samples/db-studio/ui/dist"
7775 private const val STUDIO_OUTPUT_DIR = " .db-studio"
7876 private const val STUDIO_INDEX_FILE = " index.tsx"
7977 private val SQLITE_EXTENSIONS = setOf (" .db" , " .sqlite" , " .sqlite3" , " .db3" )
@@ -158,30 +156,21 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
158156 }
159157 }
160158
161- private fun copyResourceDirectory (resourcePath : String , targetDir : Path ) {
162- val resourceUrl = this ::class .java.classLoader.getResource(resourcePath)
163- ? : error(" Resource not found: $resourcePath " )
164-
165- val uri = resourceUrl.toURI()
166- val fileSystem = when (uri.scheme) {
167- " jar" -> FileSystems .newFileSystem(uri, emptyMap<String , Any >())
168- else -> null
159+ private fun copyDirectory (sourcePath : Path , targetDir : Path ) {
160+ if (! sourcePath.exists() || ! sourcePath.isDirectory()) {
161+ error(" Source directory not found: $sourcePath " )
169162 }
170163
171- fileSystem.use {
172- val sourcePath = fileSystem?.getPath(resourcePath) ? : Path .of(uri)
173-
174- Files .walk(sourcePath).use { stream ->
175- stream.forEach { source ->
176- val relative = sourcePath.relativize(source)
177- val target = targetDir.resolve(relative.toString())
164+ Files .walk(sourcePath).use { stream ->
165+ stream.forEach { source ->
166+ val relative = sourcePath.relativize(source)
167+ val target = targetDir.resolve(relative.toString())
178168
179- when {
180- source.isDirectory() -> Files .createDirectories(target)
181- else -> {
182- Files .createDirectories(target.parent)
183- Files .copy(source, target, StandardCopyOption .REPLACE_EXISTING )
184- }
169+ when {
170+ source.isDirectory() -> Files .createDirectories(target)
171+ else -> {
172+ Files .createDirectories(target.parent)
173+ Files .copy(source, target, StandardCopyOption .REPLACE_EXISTING )
185174 }
186175 }
187176 }
@@ -203,58 +192,97 @@ internal class DbStudioCommand : AbstractSubcommand<ToolState, CommandContext>()
203192 )
204193 internal var port: Int = 4983
205194
195+ @Option(
196+ names = [" --api-port" ],
197+ description = [" Port to run the API server on" ],
198+ defaultValue = " 4984" ,
199+ )
200+ internal var apiPort: Int = 4984
201+
206202 internal var host: String = " localhost"
207203
208204 override suspend fun CommandContext.invoke (state : ToolContext <ToolState >): CommandResult {
209205 val outputDir = Path .of(STUDIO_OUTPUT_DIR )
210- copyResourceDirectory(STUDIO_RESOURCE_PATH , outputDir)
211206
212- val indexFile = outputDir.resolve(STUDIO_INDEX_FILE )
207+ // Create organized directory structure
208+ val apiDir = outputDir.resolve(" api" )
209+ val uiDir = outputDir.resolve(" ui" )
210+ Files .createDirectories(apiDir)
211+ Files .createDirectories(uiDir)
212+
213+ // Copy API server files from samples/db-studio/api to .db-studio/api/
214+ val apiSource = Path .of(STUDIO_API_SOURCE )
215+ if (! apiSource.exists() || ! apiSource.isDirectory()) {
216+ return CommandResult .err(
217+ message = " API source not found at $STUDIO_API_SOURCE . Ensure samples/db-studio/api exists."
218+ )
219+ }
220+ copyDirectory(apiSource, apiDir)
221+
222+ // Copy React app UI files from samples/db-studio/ui/dist to .db-studio/ui/
223+ val uiSource = Path .of(STUDIO_UI_SOURCE )
224+ if (! uiSource.exists() || ! uiSource.isDirectory()) {
225+ return CommandResult .err(
226+ message = " UI build not found at $STUDIO_UI_SOURCE . Please run 'cd samples/db-studio/ui && npm run build' first."
227+ )
228+ }
229+ copyDirectory(uiSource, uiDir)
230+
231+ val indexFile = apiDir.resolve(STUDIO_INDEX_FILE )
213232 val baseContent = indexFile.readText()
214233
215- // Handle database selection mode vs direct database path
216- val processedContent = if (databasePath == null ) {
217- // Discovery mode: find available databases
234+ // Always use databases array - either discover or create from single path
235+ val databases = if (databasePath == null ) {
218236 val discovered = discoverDatabases()
219237
220238 if (discovered.isEmpty()) {
221239 return CommandResult .err(message = " No SQLite databases found in current directory or user data directories" )
222240 }
223241
224- val json = Json { prettyPrint = false }
225- val databasesJson = json.encodeToString(discovered)
226-
227- baseContent
228- .replace(" \" __DB_PATH__\" " , " null" )
229- .replace(" __PORT__" , port.toString())
230- .replace(" \" __DATABASES__\" " , databasesJson)
231- .replace(" __SELECTION_MODE__" , " true" )
242+ discovered
232243 } else {
233- // Direct path mode: use provided database
234244 val dbFile = Path .of(databasePath!! )
235245
236246 if (! dbFile.exists()) {
237247 return CommandResult .err(message = " Database file not found: $databasePath " )
238248 }
239249
240- val absoluteDbPath = dbFile.toAbsolutePath().toString()
241-
242- baseContent
243- .replace(" \" __DB_PATH__\" " , " \" $absoluteDbPath \" " )
244- .replace(" __PORT__" , port.toString())
245- .replace(" \" __DATABASES__\" " , " []" )
246- .replace(" __SELECTION_MODE__" , " false" )
250+ // Create a single-item list with the specified database
251+ listOf (
252+ DiscoveredDatabase (
253+ path = dbFile.toAbsolutePath().toString(),
254+ name = dbFile.name,
255+ size = dbFile.fileSize(),
256+ lastModified = dbFile.getLastModifiedTime().toMillis(),
257+ isLocal = true ,
258+ )
259+ )
247260 }
248261
262+ val json = Json { prettyPrint = false }
263+ val databasesJson = json.encodeToString(databases)
264+
265+ val processedContent = baseContent
266+ .replace(" __PORT__" , apiPort.toString())
267+ .replace(" \" __DATABASES__\" " , databasesJson)
268+
249269 indexFile.writeText(processedContent)
250270
251271 output {
252- appendLine(" Generated database studio in: ${outputDir.toAbsolutePath()} " )
272+ appendLine(" Database Studio files generated in: ${outputDir.toAbsolutePath()} " )
273+ appendLine()
274+ appendLine(" To start the Database Studio:" )
275+ appendLine()
276+ appendLine(" Terminal 1 (API Server on port $apiPort ):" )
277+ appendLine(" cd ${apiDir.toAbsolutePath()} " )
278+ appendLine(" elide serve $STUDIO_INDEX_FILE " )
279+ appendLine()
280+ appendLine(" Terminal 2 (UI Server on port 8080):" )
281+ appendLine(" cd ${uiDir.toAbsolutePath()} " )
282+ appendLine(" elide serve ." )
253283 appendLine()
254- appendLine(" To start the database UI, run:" )
255- appendLine(" elide serve $STUDIO_OUTPUT_DIR /$STUDIO_INDEX_FILE " )
284+ appendLine(" Then open: http://localhost:8080" )
256285 appendLine()
257- appendLine(" Then open: http://$host :$port " )
258286 }
259287
260288 return CommandResult .success()
0 commit comments