diff --git a/packages/kilo-jetbrains/backend/build.gradle.kts b/packages/kilo-jetbrains/backend/build.gradle.kts index b4cae1beac..a99ed452c8 100644 --- a/packages/kilo-jetbrains/backend/build.gradle.kts +++ b/packages/kilo-jetbrains/backend/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { } val generatedApi = layout.buildDirectory.dir("generated/openapi/src/main/kotlin") +val generatedSpec = layout.buildDirectory.file("generated/openapi-spec/openapi.json") sourceSets { main { @@ -19,10 +20,17 @@ sourceSets { } } +val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) { + description = "Generate CLI OpenAPI spec into the build directory" + opencodeDir.set(rootProject.layout.projectDirectory.dir("../opencode")) + serverSrcDir.set(rootProject.layout.projectDirectory.dir("../opencode/src/server")) + spec.set(generatedSpec) +} + openApiGenerate { generatorName.set("kotlin") library.set("jvm-okhttp4") - inputSpec.set("${rootDir}/../sdk/openapi.json") + inputSpec.set(generatedSpec.map { it.asFile.absolutePath }) outputDir.set(layout.buildDirectory.dir("generated/openapi").get().asFile.absolutePath) packageName.set("ai.kilocode.jetbrains.api") apiPackage.set("ai.kilocode.jetbrains.api.client") @@ -54,6 +62,10 @@ openApiGenerate { generateModelDocumentation.set(false) } +tasks.named("openApiGenerate") { + dependsOn(generateOpenApiSpec) +} + val fixGeneratedApi by tasks.registering(FixGeneratedApiTask::class) { dependsOn("openApiGenerate") generated.set(generatedApi) diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt new file mode 100644 index 0000000000..f594d0f06b --- /dev/null +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt @@ -0,0 +1,104 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject +import org.gradle.process.ExecOperations +import java.io.ByteArrayOutputStream + +/** + * Generates the CLI OpenAPI spec into the build directory so the JetBrains + * Gradle build is self-contained and does not mutate the tracked + * packages/sdk/openapi.json. + * + * Runs `bun dev generate` from the opencode package directory and captures + * stdout to [spec]. stderr is captured separately and included in the error + * message on failure. + * + * Gradle up-to-date tracking is scoped to [serverSrcDir] (the opencode server + * source) to avoid busting the cache on unrelated changes to dist/, node_modules/, + * etc. + */ +abstract class GenerateOpenApiSpecTask : DefaultTask() { + + /** + * The server source directory inside the opencode package — the only files + * that affect the OpenAPI output. Scoped to `src/server/` to avoid busting + * the Gradle up-to-date check on unrelated file changes (dist/, node_modules/). + */ + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val serverSrcDir: DirectoryProperty + + /** + * Root of the `packages/opencode/` package — the working directory for bun. + * Marked @Internal because it is not itself a Gradle input; only [serverSrcDir] + * (a subdirectory) participates in up-to-date checking. + */ + @get:Internal + abstract val opencodeDir: DirectoryProperty + + /** Destination file for the generated openapi.json. */ + @get:OutputFile + abstract val spec: RegularFileProperty + + @get:Inject + abstract val exec: ExecOperations + + @TaskAction + fun run() { + val out = ByteArrayOutputStream() + val err = ByteArrayOutputStream() + val result = exec.exec { + workingDir = opencodeDir.get().asFile + commandLine(findBun(), "run", "--conditions=browser", "./src/index.ts", "generate") + standardOutput = out + errorOutput = err + isIgnoreExitValue = true + } + if (result.exitValue != 0) { + throw GradleException( + "bun dev generate failed with exit code ${result.exitValue}.\n" + + err.toString(Charsets.UTF_8).take(2000) + ) + } + val json = out.toString(Charsets.UTF_8) + if (!json.trimStart().startsWith("{")) { + throw GradleException( + "bun dev generate did not produce JSON.\n" + + "stdout: ${json.take(200)}\n" + + "stderr: ${err.toString(Charsets.UTF_8).take(500)}" + ) + } + spec.get().asFile.also { it.parentFile.mkdirs() }.writeText(json) + } + + private fun findBun(): String { + val which = runCatching { + ProcessBuilder("which", "bun") + .redirectErrorStream(true) + .start() + .inputStream.bufferedReader().readLine()?.trim() + }.getOrNull() + if (which != null && java.io.File(which).isFile) return which + + val home = System.getProperty("user.home") + val candidates = listOf( + "$home/.bun/bin/bun", + "/opt/homebrew/bin/bun", + "/usr/local/bin/bun", + "$home/.nvm/current/bin/bun", + ) + for (path in candidates) { + val f = java.io.File(path) + if (f.isFile && f.canExecute()) return f.absolutePath + } + return "bun" + } +} diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 34b33a8422..bbfe481a8c 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "build": "bun script/build.ts", - "build:production": "bun script/build.ts --production" + "build:production": "bun script/build.ts --production", + "typecheck": "bun run build" } } diff --git a/turbo.json b/turbo.json index 90cf038a0c..6f7406a9bc 100644 --- a/turbo.json +++ b/turbo.json @@ -28,6 +28,10 @@ "@kilocode/kilo-jetbrains#build": { "dependsOn": ["@kilocode/cli#build"], "outputs": ["build/distributions/**"] + }, + "@kilocode/kilo-jetbrains#typecheck": { + "dependsOn": ["@kilocode/cli#build"], + "outputs": [] } } }