fix(jetbrains): resolve Bun path for repo CLI generation under IDE Gradle runs

IDE-launched Gradle runs (e.g. Run IDE Split Mode) can have a stripped
PATH where 'bun' isn't resolvable, so :backend:generateOpenApiSpec and
:backend:buildRepoCli failed with 'A problem occurred starting process
command bun' in repo-CLI (unpinned) mode.

The jetbrains-cli-pin skill's unpin/regen commands now write an
ignored, worktree-local Bun path hint (.gradle/kilo-cli-pin.properties)
that backend/build.gradle.kts resolves and passes as an absolute path
to the affected tasks. pin removes the hint since pinned mode doesn't
depend on local Bun.
This commit is contained in:
kirillk
2026-08-05 09:37:44 -04:00
parent a340d61716
commit 7ee1909f73
4 changed files with 50 additions and 5 deletions
+16 -3
View File
@@ -34,9 +34,9 @@ bun .kilo/skills/jetbrains-cli-pin/script/cli-pin.ts <command> [--no-verify]
| Command | Steps |
|---|---|
| `pin` | Clean -> set `kilo.cli.pinned=true` -> bump `package.json` to latest release (via `set-pin.ts --latest`, which validates release assets) -> verify with a cold `gradlew clean typecheck`. |
| `unpin` | Clean -> set `kilo.cli.pinned=false` -> `:backend:buildRepoCli` (fresh CLI) -> `:backend:stageRepoCli` -> assert staged `kilo-cli.zip` -> verify with `gradlew typecheck`. |
| `regen` | Fast dev loop while unpinned: `rm -rf dist` -> `buildRepoCli` -> `stageRepoCli`. Refuses to run unless `kilo.cli.pinned=false`. |
| `pin` | Clean -> set `kilo.cli.pinned=true` -> remove the repo-CLI Bun path hint -> bump `package.json` to latest release (via `set-pin.ts --latest`, which validates release assets) -> verify with a cold `gradlew clean typecheck`. |
| `unpin` | Clean -> set `kilo.cli.pinned=false` -> write the repo-CLI Bun path hint -> `:backend:buildRepoCli` (fresh CLI) -> `:backend:stageRepoCli` -> assert staged `kilo-cli.zip` -> verify with `gradlew typecheck`. |
| `regen` | Fast dev loop while unpinned: refresh the repo-CLI Bun path hint -> `rm -rf dist` -> `buildRepoCli` -> `stageRepoCli`. Refuses to run unless `kilo.cli.pinned=false`. |
| `clean` | Run the shared artifact clean only. |
`--no-verify` skips the gradle verification build (rewrites + clean only). Use it when
@@ -60,6 +60,19 @@ The staged `kilo-cli.zip` is the nastiest leak: once it lands in `backend/build/
from an unpinned build, runtime prefers the bundled zip over downloading. A full clean is
the only reliable reset.
## Bun Path Hint
In repo CLI mode, Gradle's `generateOpenApiSpec` task runs the local CLI source through
`bun run --conditions=browser ./src/index.ts generate`. IDE-launched Gradle runs can have
worktree-local hint:
```text
packages/kilo-jetbrains/.gradle/kilo-cli-pin.properties
```
The file contains `bun.path=<absolute path>` and is consumed by `backend/build.gradle.kts`
for repo CLI tasks. `pin` removes it because pinned mode should not depend on local Bun.
## Notes
- Verification builds pass `--no-configuration-cache` so the changed `kilo.cli.pinned`
@@ -8,6 +8,7 @@ const jb = "packages/kilo-jetbrains"
const props = `${jb}/gradle.properties`
const pkg = `${jb}/package.json`
const zip = `${jb}/backend/build/generated/kilo-cli-res/kilo-cli.zip`
const hint = `${jb}/.gradle/kilo-cli-pin.properties`
const arg = Bun.argv[2]
const cmd = arg && !arg.startsWith("-") ? arg : undefined
@@ -52,6 +53,23 @@ async function setPinned(value: boolean) {
await Bun.write(props, text.replace(/^kilo\.cli\.pinned=.*$/m, `kilo.cli.pinned=${value}`))
}
async function bunPath() {
const result = await $`command -v bun`.quiet().nothrow()
const bin = result.exitCode === 0 ? result.stdout.toString().trim() : ""
return bin || process.execPath
}
async function writeBunHint() {
const path = await bunPath()
await $`mkdir -p ${jb}/.gradle`
await Bun.write(hint, `# Generated by jetbrains-cli-pin so IDE-launched Gradle can find Bun in repo CLI mode.\nbun.path=${path}\n`)
console.log(`Wrote Bun path hint for repo CLI mode: ${path}`)
}
async function removeBunHint() {
await $`rm -f ${hint}`.nothrow()
}
async function report() {
const version = (await Bun.file(pkg).json()).version
console.log(`\nState: kilo.cli.pinned=${await pinned()}, package.json version=${version}`)
@@ -60,6 +78,7 @@ async function report() {
if (cmd === "pin") {
await clean()
await setPinned(true)
await removeBunHint()
// set-pin.ts bumps package.json to the latest release and refuses versions with
// missing runtime assets, so we do not reimplement release/asset validation.
await $`bun .kilo/skills/release-jetbrains/script/set-pin.ts --latest`
@@ -71,6 +90,7 @@ if (cmd === "pin") {
} else if (cmd === "unpin") {
await clean()
await setPinned(false)
await writeBunHint()
// build.ts does rm -rf dist internally, producing a fresh single-platform binary.
await $`./gradlew :backend:buildRepoCli --no-configuration-cache`.cwd(jb)
// stageRepoCli has upToDateWhen{false}; force it so the staged zip matches this build.
@@ -82,6 +102,7 @@ if (cmd === "pin") {
await report()
} else if (cmd === "regen") {
if (await pinned()) throw new Error("regen requires the unpinned state; run 'unpin' first")
await writeBunHint()
await $`rm -rf packages/opencode/dist`
await $`./gradlew :backend:buildRepoCli --no-configuration-cache`.cwd(jb)
await $`./gradlew :backend:stageRepoCli --no-configuration-cache`.cwd(jb)
@@ -26,6 +26,13 @@ val repoCli = pinned.map { !it }
val bundled = providers.gradleProperty("kilo.cli.bundled").map { it.trim().toBoolean() }.orElse(false)
val downloadsCli = repoCli.zip(bundled) { repo, bundle -> !repo && !bundle }
val repoRootDir = rootProject.layout.projectDirectory.dir("../opencode")
val local = rootProject.layout.projectDirectory.file(".gradle/kilo-cli-pin.properties")
val bunPathProvider = providers.fileContents(local).asText.map { text ->
text.lineSequence().firstNotNullOfOrNull { line ->
val pair = line.split("=", limit = 2)
if (pair.getOrNull(0)?.trim() == "bun.path") pair.getOrNull(1)?.trim()?.takeIf { it.isNotEmpty() } else null
} ?: "bun"
}.orElse("bun")
val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirectory.file("package.json")).asText.map { text ->
Regex("\"version\"\\s*:\\s*\"([^\"]+)\"").find(text)?.groupValues?.get(1)
@@ -64,12 +71,13 @@ val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) {
)
cacheDir.set(layout.buildDirectory.dir("cli-cache"))
spec.set(rawSpec)
bunPath.set(bunPathProvider)
}
val buildRepoCli by tasks.registering(Exec::class) {
description = "Build the local repo CLI for the current platform"
workingDir = repoRootDir.asFile
commandLine("bun", "run", "script/build.ts", "--single", "--skip-install")
commandLine(bunPathProvider.get(), "run", "script/build.ts", "--single", "--skip-install")
}
fun platform(): String {
@@ -49,6 +49,9 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
@get:Internal
abstract val cacheDir: DirectoryProperty
@get:Internal
abstract val bunPath: Property<String>
@get:OutputFile
abstract val spec: RegularFileProperty
@@ -76,7 +79,7 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
val err = ByteArrayOutputStream()
val result = exec.exec {
workingDir = root
commandLine("bun", "run", "--conditions=browser", "./src/index.ts", "generate")
commandLine(bunPath.get(), "run", "--conditions=browser", "./src/index.ts", "generate")
standardOutput = out
errorOutput = err
isIgnoreExitValue = true