diff --git a/bun.lock b/bun.lock index a78c9245ce..9b06a85a6c 100644 --- a/bun.lock +++ b/bun.lock @@ -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"], diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md new file mode 100644 index 0000000000..85cef069ea --- /dev/null +++ b/packages/kilo-jetbrains/README.md @@ -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 +``` diff --git a/packages/kilo-jetbrains/backend/build.gradle.kts b/packages/kilo-jetbrains/backend/build.gradle.kts index 71c776146a..383a5497ee 100644 --- a/packages/kilo-jetbrains/backend/build.gradle.kts +++ b/packages/kilo-jetbrains/backend/build.gradle.kts @@ -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) diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index 0c1e494600..893575eb1f 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -50,3 +50,5 @@ intellijPlatform { } } } + + diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json new file mode 100644 index 0000000000..34b33a8422 --- /dev/null +++ b/packages/kilo-jetbrains/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kilocode/kilo-jetbrains", + "private": true, + "scripts": { + "build": "bun script/build.ts", + "build:production": "bun script/build.ts --production" + } +} diff --git a/packages/kilo-jetbrains/script/build.ts b/packages/kilo-jetbrains/script/build.ts new file mode 100644 index 0000000000..fe0dfb1d2c --- /dev/null +++ b/packages/kilo-jetbrains/script/build.ts @@ -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) +} diff --git a/turbo.json b/turbo.json index bf1c18ab91..03f3aa6dca 100644 --- a/turbo.json +++ b/turbo.json @@ -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/**"] } } }