Skip to content
This repository was archived by the owner on Jul 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CliIntegrationTest {
assertThat(createResult.output)
.contains("${projectGradleScript()} :desktopApp:hotRunJvm --auto")
assertJvmReadme(projectDir)
assertGeneratedProjectHasGitignore(projectDir)

val compileResult =
runProcess(
Expand Down Expand Up @@ -288,11 +289,13 @@ class CliIntegrationTest {
val rootDir = createTempRoot("composables-cli-mcp-install-cursor")
try {
val launcher = installedLauncher()
File(rootDir, "settings.gradle.kts").writeText("""pluginManagement {}""")
val nestedDir = File(rootDir, "features/chat").apply { mkdirs() }

val result =
runProcess(
command = listOf(launcher.absolutePath, "mcp", "install", "--client", "cursor"),
workingDir = rootDir,
workingDir = nestedDir,
timeoutSeconds = 60,
)

Expand All @@ -309,24 +312,45 @@ class CliIntegrationTest {
}

@Test
fun `cli mcp install writes firebender global config`() {
fun `cli mcp install fails outside a Gradle project for project clients`() {
val rootDir = createTempRoot("composables-cli-mcp-install-no-project")
try {
val launcher = installedLauncher()

val result =
runProcess(
command = listOf(launcher.absolutePath, "mcp", "install", "--client", "cursor"),
workingDir = rootDir,
timeoutSeconds = 60,
)

assertThat(result.finished).isTrue()
assertThat(result.exitCode).isEqualTo(1)
assertThat(result.output).contains("Could not find a Gradle project root")
assertThat(result.output).contains("Run this command from the root of your project")
} finally {
rootDir.deleteRecursively()
}
}

@Test
fun `cli mcp install writes firebender project config`() {
val rootDir = createTempRoot("composables-cli-mcp-install-firebender")
try {
val launcher = installedLauncher()
File(rootDir, "settings.gradle.kts").writeText("""pluginManagement {}""")

val result =
runProcess(
command = listOf(launcher.absolutePath, "mcp", "install", "--client", "firebender"),
workingDir = rootDir,
extraEnvironment =
mapOf("JAVA_TOOL_OPTIONS" to "-Duser.home=${rootDir.absolutePath}"),
timeoutSeconds = 60,
)

assertThat(result.finished).isTrue()
assertThat(result.exitCode).isEqualTo(0)
assertThat(result.output).contains("Installed Composables MCP for Firebender.")
val content = File(rootDir, ".firebender/firebender.json").readText()
val content = File(rootDir, "firebender.json").readText()
assertThat(content).contains(""""mcpServers"""")
assertThat(content).contains(""""command": "composables"""")
assertThat(content).contains(""""mcp"""")
Expand All @@ -337,10 +361,11 @@ class CliIntegrationTest {
}

@Test
fun `cli mcp install writes zed global config`() {
fun `cli mcp install writes zed project config`() {
val rootDir = createTempRoot("composables-cli-mcp-install-zed")
try {
val launcher = installedLauncher()
File(rootDir, "settings.gradle.kts").writeText("""pluginManagement {}""")

val result =
runProcess(
Expand All @@ -353,16 +378,13 @@ class CliIntegrationTest {
"zed",
),
workingDir = rootDir,
extraEnvironment =
mapOf(
"JAVA_TOOL_OPTIONS" to "-Duser.home=${rootDir.absolutePath} -Dos.name=Linux"),
timeoutSeconds = 60,
)

assertThat(result.finished).isTrue()
assertThat(result.exitCode).isEqualTo(0)
assertThat(result.output).contains("Installed Composables MCP for Zed.")
val content = File(rootDir, ".config/zed/settings.json").readText()
val content = File(rootDir, ".zed/settings.json").readText()
assertThat(content).contains(""""context_servers"""")
assertThat(content).contains(""""command": "composables"""")
assertThat(content).contains(""""mcp"""")
Expand Down Expand Up @@ -1046,6 +1068,16 @@ class CliIntegrationTest {
.contains("fun main() = singleWindowApplication(title = \"Sample App\") { App() }")
}

private fun assertGeneratedProjectHasGitignore(projectDir: File) {
val gitignore = File(projectDir, ".gitignore")
assertThat(gitignore.exists()).isTrue()

val content = gitignore.readText()
assertThat(content).contains(".idea")
assertThat(content).contains(".gradle")
assertThat(content).contains("**/build/")
}

private fun runProcess(
command: List<String>,
workingDir: File,
Expand Down
82 changes: 57 additions & 25 deletions cli/src/jvmMain/kotlin/Cli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -494,37 +494,65 @@ class McpInstall : CliktCommand("install") {
}

"antigravity" -> {
val result = installAntigravityMcpServer(overwrite = overwrite)
val result =
installAntigravityMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Antigravity", result)
}

"claude" -> {
val result = installClaudeCodeMcpServer(projectRoot = workingDir, overwrite = overwrite)
val result =
installClaudeCodeMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Claude Code", result)
}

"codex" -> {
val result = installCodexMcpServer(projectRoot = workingDir, overwrite = overwrite)
val result =
installCodexMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Codex", result)
}

"cursor" -> {
val result = installCursorMcpServer(projectRoot = workingDir, overwrite = overwrite)
val result =
installCursorMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Cursor", result)
}

"firebender" -> {
val result = installFirebenderMcpServer(overwrite = overwrite)
val result =
installFirebenderMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Firebender", result)
}

"opencode" -> {
val result = installOpenCodeMcpServer(projectRoot = workingDir, overwrite = overwrite)
val result =
installOpenCodeMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("OpenCode", result)
}

"zed" -> {
val result = installZedMcpServer(overwrite = overwrite)
val result =
installZedMcpServer(
projectRoot = findRequiredMcpProjectRoot(workingDir),
overwrite = overwrite,
)
echoInstallResult("Zed", result)
}

Expand All @@ -549,6 +577,22 @@ class McpInstall : CliktCommand("install") {
}
}

internal fun findGradleProjectRoot(start: File): File? {
var current: File? = start.absoluteFile
while (current != null) {
if (File(current, "settings.gradle.kts").isFile || File(current, "settings.gradle").isFile) {
return current
}
current = current.parentFile
}
return null
}

private fun findRequiredMcpProjectRoot(start: File): File =
findGradleProjectRoot(start)
?: throw UsageError(
"Could not find a Gradle project root. Run this command from the root of your project.")

class AddModule : CliktCommand("module") {
override fun help(context: Context): String =
"""
Expand Down Expand Up @@ -810,12 +854,12 @@ internal fun installAndroidStudioMcpServer(
}

internal fun installAntigravityMcpServer(
configFile: File = File(System.getProperty("user.home"), ".gemini/config/mcp_config.json"),
projectRoot: File,
serverName: String = "composables",
overwrite: Boolean = false,
): McpInstallResult =
installJsonMcpServer(
configFile = configFile,
configFile = File(projectRoot, ".agents/mcp_config.json"),
rootKey = "mcpServers",
serverName = serverName,
serverConfig = composablesStdioJsonConfig,
Expand Down Expand Up @@ -868,12 +912,12 @@ internal fun installOpenCodeMcpServer(
)

internal fun installFirebenderMcpServer(
configFile: File = File(System.getProperty("user.home"), ".firebender/firebender.json"),
projectRoot: File,
serverName: String = "composables",
overwrite: Boolean = false,
): McpInstallResult =
installJsonMcpServer(
configFile = configFile,
configFile = File(projectRoot, "firebender.json"),
rootKey = "mcpServers",
serverName = serverName,
serverConfig = composablesStdioJsonConfig,
Expand All @@ -882,31 +926,19 @@ internal fun installFirebenderMcpServer(
)

internal fun installZedMcpServer(
configFile: File = defaultZedSettingsFile(),
projectRoot: File,
serverName: String = "composables",
overwrite: Boolean = false,
): McpInstallResult =
installJsonMcpServer(
configFile = configFile,
configFile = File(projectRoot, ".zed/settings.json"),
rootKey = "context_servers",
serverName = serverName,
serverConfig = composablesStdioJsonConfig,
clientName = "Zed",
overwrite = overwrite,
)

internal fun defaultZedSettingsFile(appDataPath: String? = System.getenv("APPDATA")): File {
val homeDir = File(System.getProperty("user.home"))
return when {
isWindows() -> {
val appData = appDataPath?.takeIf { it.isNotBlank() }?.let(::File)
File(appData ?: File(homeDir, "AppData/Roaming"), "Zed/settings.json")
}
isMacOs() -> File(homeDir, ".zed/settings.json")
else -> File(homeDir, ".config/zed/settings.json")
}
}

private fun installJsonMcpServer(
configFile: File,
rootKey: String,
Expand Down
26 changes: 14 additions & 12 deletions cli/src/jvmMain/resources/project/.gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
*.iml
.kotlin
.gradle
.kotlin/
/local.properties
/.idea
**/build/
xcuserdata
!src/**/build/
local.properties
.idea
.DS_Store
kotlin-js-store/
build/
/captures
captures
.externalNativeBuild
.cxx
iosApp/Podfile.lock
iosApp/Pods/*
iosApp/iosApp.xcworkspace/*
iosApp/iosApp.xcodeproj/*
!iosApp/iosApp.xcodeproj/project.pbxproj
shared/shared.podspec
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
Loading
Loading