feat(jetbrains): add CLI binary bundling and build pipeline

Adds a build system that compiles Kilo CLI binaries and packages them
into the backend jar at /cli/{os}/kilo for runtime extraction.

- script/build.ts: orchestrates CLI build + Gradle plugin build
- Local mode (bun run build): builds current platform only
- Production mode (bun run build:production): requires all 6 platforms
- Gradle checkCli task validates binaries before processResources
- Turbo integration via @kilocode/kilo-jetbrains#build
- README with setup and build instructions
This commit is contained in:
kirillk
2026-04-02 12:10:12 -04:00
parent 4cd3d15317
commit c06d8e0fa6
7 changed files with 265 additions and 0 deletions
+5
View File
@@ -214,6 +214,9 @@
"typescript": "catalog:",
},
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.1.17",
@@ -1211,6 +1214,8 @@
"@kilocode/kilo-i18n": ["@kilocode/kilo-i18n@workspace:packages/kilo-i18n"],
"@kilocode/kilo-jetbrains": ["@kilocode/kilo-jetbrains@workspace:packages/kilo-jetbrains"],
"@kilocode/kilo-telemetry": ["@kilocode/kilo-telemetry@workspace:packages/kilo-telemetry"],
"@kilocode/kilo-ui": ["@kilocode/kilo-ui@workspace:packages/kilo-ui"],
+69
View File
@@ -0,0 +1,69 @@
# Kilo JetBrains
AI coding agent plugin for JetBrains IDEs.
---
## Set up your environment
### Prerequisites
- **Bun** -- used to build CLI binaries and run build scripts
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK
- **IntelliJ IDEA** -- to run the plugin in a sandboxed IDE
---
## Open in IntelliJ
When you open the monorepo root in IntelliJ IDEA, the Gradle project at `packages/kilo-jetbrains/` should be auto-detected via `.idea/gradle.xml`. If not, link it manually: **File > Settings > Build Tools > Gradle > +** and select `packages/kilo-jetbrains/settings.gradle.kts`.
---
## Build locally
From `packages/kilo-jetbrains/`:
```
bun run build
```
This builds the CLI binary for your current OS/arch only, copies it into the backend module resources, and runs `./gradlew buildPlugin`. The plugin archive is output to `build/distributions/`.
Or via Turbo from the repo root:
```
bun turbo build --filter=@kilocode/kilo-jetbrains
```
---
## Build for production
From `packages/kilo-jetbrains/`:
```
bun run build:production
```
This builds CLI binaries for all 6 desktop platforms (darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64, windows-arm64), copies them all into the backend jar, and fails if any are missing. Gradle also validates all platforms are present via `-Pproduction=true`.
---
## Run the plugin
After building, use the `runIde` Gradle task (available in the Gradle tool window or via the "Run JetBrains Plugin" run configuration) to launch a sandboxed IntelliJ instance with the plugin installed.
Note: `runIde` does not build CLI binaries -- run `bun run build` at least once before using it. Subsequent Kotlin/Gradle changes can be iterated with `runIde` directly.
---
## Run Gradle directly
You can run `./gradlew buildPlugin` directly if CLI binaries are already in `backend/build/generated/cli/`. Gradle will fail with a clear error if they are missing.
For production verification:
```
./gradlew buildPlugin -Pproduction=true
```
@@ -6,6 +6,55 @@ kotlin {
jvmToolchain(21)
}
sourceSets {
main {
resources.srcDir(layout.buildDirectory.dir("generated/cli"))
}
}
val cliDir = layout.buildDirectory.dir("generated/cli/cli")
val production = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false)
val requiredPlatforms = listOf(
"darwin-arm64",
"darwin-x64",
"linux-arm64",
"linux-x64",
"windows-x64",
"windows-arm64",
)
val checkCli by tasks.registering {
description = "Verify CLI binaries exist before building"
val dir = cliDir.map { it.asFile }
val prod = production.get()
val platforms = requiredPlatforms.toList()
doLast {
val resolved = dir.get()
if (!resolved.exists() || resolved.listFiles()?.isEmpty() != false) {
throw GradleException(
"CLI binaries not found at ${resolved.absolutePath}.\n" +
"Run 'bun run build' from packages/kilo-jetbrains/ to build CLI and plugin together."
)
}
if (prod) {
val present = resolved.listFiles()?.map { it.name }?.toSet() ?: emptySet()
val missing = platforms.filter { it !in present }
if (missing.isNotEmpty()) {
throw GradleException(
"Production build requires all platform CLI binaries.\n" +
"Missing: ${missing.joinToString(", ")}\n" +
"Run 'bun run build:production' to build all platforms."
)
}
}
}
}
tasks.processResources {
dependsOn(checkCli)
}
dependencies {
intellijPlatform {
intellijIdea(libs.versions.intellij.platform)
+2
View File
@@ -50,3 +50,5 @@ intellijPlatform {
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@kilocode/kilo-jetbrains",
"private": true,
"scripts": {
"build": "bun script/build.ts",
"build:production": "bun script/build.ts --production"
}
}
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bun
/**
* Build the Kilo JetBrains plugin.
*
* Usage:
* bun script/build.ts # Local build — only current platform binary required
* bun script/build.ts --production # Production build — all 6 platform binaries required
*
* Steps:
* 1. Builds CLI binaries (or uses prebuilt ones from dist/).
* Local: builds only current platform (--single).
* Production: builds all platforms.
* 2. Copies them into backend/build/generated/cli/cli/{os}/kilo[.exe]
* so they end up inside the backend jar at /cli/{os}/kilo.
* 3. Invokes Gradle to build the plugin.
*/
import { $ } from "bun"
import { join, relative } from "node:path"
import { existsSync, mkdirSync, chmodSync, cpSync, rmSync } from "node:fs"
const production = process.argv.includes("--production")
const root = join(import.meta.dir, "..")
const packages = join(root, "..")
const opencodeDir = join(packages, "opencode")
const distDir = join(opencodeDir, "dist")
const cliDir = join(root, "backend", "build", "generated", "cli", "cli")
/** All desktop platforms. */
const platforms = [
{ os: "darwin-arm64", exe: "kilo" },
{ os: "darwin-x64", exe: "kilo" },
{ os: "linux-arm64", exe: "kilo" },
{ os: "linux-x64", exe: "kilo" },
{ os: "windows-x64", exe: "kilo.exe" },
{ os: "windows-arm64", exe: "kilo.exe" },
] as const
function localPlatformTag(): string {
const os = process.platform === "win32" ? "windows" : process.platform
return `${os}-${process.arch}`
}
function log(msg: string) {
console.log(`[jetbrains-build] ${msg}`)
}
function distBinPath(os: string, exe: string): string {
return join(distDir, `@kilocode/cli-${os}`, "bin", exe)
}
function hasDist(): boolean {
if (production) {
return platforms.every((p) => existsSync(distBinPath(p.os, p.exe)))
}
const tag = localPlatformTag()
const local = platforms.find((p) => p.os === tag)
return local ? existsSync(distBinPath(local.os, local.exe)) : false
}
async function prepareCli() {
const mode = production ? "production" : "local"
log(`Mode: ${mode}`)
if (!hasDist()) {
log("Building CLI binaries via opencode...")
if (!existsSync(join(opencodeDir, "package.json"))) {
throw new Error(`Expected opencode package at ${opencodeDir}`)
}
const args = production ? [] : ["--single"]
await $`bun run build ${args}`.cwd(opencodeDir)
} else {
log("Found prebuilt CLI binaries in opencode/dist/, skipping CLI build")
}
if (existsSync(cliDir)) {
rmSync(cliDir, { recursive: true })
}
const missing: string[] = []
let copied = 0
for (const p of platforms) {
const src = distBinPath(p.os, p.exe)
if (!existsSync(src)) {
missing.push(p.os)
continue
}
const dir = join(cliDir, p.os)
mkdirSync(dir, { recursive: true })
const dest = join(dir, p.exe)
cpSync(src, dest)
chmodSync(dest, 0o755)
copied++
log(`Copied ${relative(root, src)} -> ${relative(root, dest)}`)
}
if (copied === 0) {
throw new Error("No CLI binaries were copied — the build cannot proceed")
}
if (production && missing.length > 0) {
throw new Error(`Production build requires all platform binaries. Missing: ${missing.join(", ")}`)
}
if (missing.length > 0) {
log(`Skipped ${missing.length} platforms (not needed for local build): ${missing.join(", ")}`)
}
log(`Copied ${copied}/${platforms.length} platform binaries`)
}
async function buildPlugin() {
log("Building JetBrains plugin via Gradle...")
const args = production ? ["-Pproduction=true"] : []
await $`./gradlew buildPlugin ${args}`.cwd(root)
log("Done. Plugin archive is in build/distributions/")
}
try {
await prepareCli()
await buildPlugin()
} catch (err) {
console.error(`[jetbrains-build] ERROR: ${err instanceof Error ? err.message : String(err)}`)
process.exit(1)
}
+4
View File
@@ -22,6 +22,10 @@
"dependsOn": ["^build"],
"outputs": [".next/**"],
"env": ["POSTHOG_API_KEY", "FREE_TIER_AMOUNT", "NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_HOST"]
},
"@kilocode/kilo-jetbrains#build": {
"dependsOn": ["@kilocode/cli#build"],
"outputs": ["build/distributions/**"]
}
}
}