Skip to main content
Glama
DependencyResolver.kt7.08 kB
package maestro.cli.util import maestro.orchestra.ApplyConfigurationCommand import maestro.orchestra.CompositeCommand import maestro.orchestra.MaestroCommand import maestro.orchestra.yaml.MaestroFlowParser import java.nio.file.Files import java.nio.file.Path import java.nio.file.LinkOption import kotlin.io.path.exists object DependencyResolver { fun discoverAllDependencies(flowFile: Path): List<Path> { val discoveredFiles = mutableSetOf<Path>() val filesToProcess = mutableListOf(normalizePath(flowFile)) while (filesToProcess.isNotEmpty()) { val currentFile = normalizePath(filesToProcess.removeFirst()) // Skip if we've already processed this file (prevents circular references) if (discoveredFiles.contains(currentFile)) continue // Add current file to discovered set discoveredFiles.add(currentFile) try { // Only process YAML files for dependency discovery if (!isYamlFile(currentFile)) { continue } val flowContent = Files.readString(currentFile) val commands = MaestroFlowParser.parseFlow(currentFile, flowContent) // Discover dependencies from each command val dependencies = commands.flatMap { maestroCommand -> extractDependenciesFromCommand(maestroCommand, currentFile) } val newDependencies = dependencies .map { normalizePath(it) } .filter { it.exists() && !discoveredFiles.contains(it) } filesToProcess.addAll(newDependencies) } catch (e: Exception) { // Ignore } } return discoveredFiles.toList() } private fun extractDependenciesFromCommand(maestroCommand: MaestroCommand, currentFile: Path): List<Path> { val commandDependencies = mutableListOf<Path>() // Check for runFlow commands and add the dependency if it exists (sourceDescription is not null) maestroCommand.runFlowCommand?.let { runFlow -> resolveDependencyFile(currentFile, runFlow.sourceDescription)?.let { commandDependencies.add(it) } } // Check for runScript commands and add the dependency if it exists (sourceDescription is not null) maestroCommand.runScriptCommand?.let { runScript -> resolveDependencyFile(currentFile, runScript.sourceDescription)?.let { commandDependencies.add(it) } } // Check for retry commands and add the dependency if it exists (sourceDescription is not null) maestroCommand.retryCommand?.let { retry -> resolveDependencyFile(currentFile, retry.sourceDescription)?.let { commandDependencies.add(it) } } // Check for addMedia commands and add the dependency if it exists (mediaPaths is not null) maestroCommand.addMediaCommand?.let { addMedia -> addMedia.mediaPaths.forEach { mediaPath -> resolveDependencyFile(currentFile, mediaPath)?.let { commandDependencies.add(it) } } } // Handle configuration commands (onFlowStart, onFlowComplete) maestroCommand.applyConfigurationCommand?.let { config -> config.config.onFlowStart?.commands?.forEach { startCommand -> commandDependencies.addAll(extractDependenciesFromCommand(startCommand, currentFile)) } config.config.onFlowComplete?.commands?.forEach { completeCommand -> commandDependencies.addAll(extractDependenciesFromCommand(completeCommand, currentFile)) } } // Handle ALL composite commands to extract dependencies from nested commands (RunFlow, Repeat, Retry) val command = maestroCommand.asCommand() if (command is CompositeCommand) { command.subCommands().forEach { nestedCommand -> commandDependencies.addAll(extractDependenciesFromCommand(nestedCommand, currentFile)) } } return commandDependencies } private fun resolvePath(flowPath: Path, requestedPath: String): Path { val path = flowPath.fileSystem.getPath(requestedPath) return if (path.isAbsolute) { path } else { flowPath.resolveSibling(path).toAbsolutePath() } } private fun resolveDependencyFile(currentFile: Path, requestedPath: String?): Path? { val trimmed = requestedPath?.trim() if (trimmed.isNullOrEmpty()) return null val resolved = resolvePath(currentFile, trimmed) return if (resolved.exists() && !Files.isDirectory(resolved)) resolved else null } private fun isYamlFile(path: Path): Boolean { val filename = path.fileName.toString().lowercase() return filename.endsWith(".yaml") || filename.endsWith(".yml") } private fun isJsFile(path: Path): Boolean { val filename = path.fileName.toString().lowercase() return filename.endsWith(".js") } fun getDependencySummary(flowFile: Path): String { val dependencies = discoverAllDependencies(flowFile) val mainFile = dependencies.firstOrNull { it == flowFile } val subflows = dependencies.filter { it != flowFile && isYamlFile(it) } val scripts = dependencies.filter { it != flowFile && isJsFile(it) } val otherFiles = dependencies.filter { it != flowFile && !isYamlFile(it) && !isJsFile(it) } return buildString { appendLine("Dependency discovery for: ${flowFile.fileName}") appendLine("Total files: ${dependencies.size}") if (subflows.isNotEmpty()) appendLine("Subflows: ${subflows.size}") if (scripts.isNotEmpty()) appendLine("Scripts: ${scripts.size}") if (otherFiles.isNotEmpty()) appendLine("Other files: ${otherFiles.size}") if (subflows.isNotEmpty()) { appendLine("Subflow files:") subflows.forEach { appendLine(" - ${it.fileName}") } } if (scripts.isNotEmpty()) { appendLine("Script files:") scripts.forEach { appendLine(" - ${it.fileName}") } } if (otherFiles.isNotEmpty()) { appendLine("Other files:") otherFiles.forEach { appendLine(" - ${it.fileName}") } } } } private fun normalizePath(path: Path): Path { return try { // Prefer canonical path without following symlinks path.toRealPath(LinkOption.NOFOLLOW_LINKS) } catch (e: Exception) { // Fall back to absolute normalized path if real path resolution fails path.toAbsolutePath().normalize() } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mobile-dev-inc/Maestro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server