diff --git a/.kilo/skills/jetbrains-cli-pin/SKILL.md b/.kilo/skills/jetbrains-cli-pin/SKILL.md index ecd59d67da..2fc5b06ef5 100644 --- a/.kilo/skills/jetbrains-cli-pin/SKILL.md +++ b/.kilo/skills/jetbrains-cli-pin/SKILL.md @@ -34,9 +34,9 @@ bun .kilo/skills/jetbrains-cli-pin/script/cli-pin.ts [--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=` 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` diff --git a/.kilo/skills/jetbrains-cli-pin/script/cli-pin.ts b/.kilo/skills/jetbrains-cli-pin/script/cli-pin.ts index ae293b6b1f..a7223d3cba 100644 --- a/.kilo/skills/jetbrains-cli-pin/script/cli-pin.ts +++ b/.kilo/skills/jetbrains-cli-pin/script/cli-pin.ts @@ -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) diff --git a/packages/kilo-jetbrains/backend/build.gradle.kts b/packages/kilo-jetbrains/backend/build.gradle.kts index e09355e19f..bcba0effb4 100644 --- a/packages/kilo-jetbrains/backend/build.gradle.kts +++ b/packages/kilo-jetbrains/backend/build.gradle.kts @@ -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 { diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt index 5482dfbd49..1c1181c34d 100644 --- a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/GenerateOpenApiSpecTask.kt @@ -49,6 +49,9 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() { @get:Internal abstract val cacheDir: DirectoryProperty + @get:Internal + abstract val bunPath: Property + @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