mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
feat(jetbrains): add repo CLI dev mode
This commit is contained in:
@@ -40,6 +40,14 @@ Show the resolved `version`, `kind`, and default `fromTagDefault` to the user.
|
||||
|
||||
Before dispatching prepare, verify the JetBrains plugin is pinned to the intended Kilo Core release. The plugin downloads the CLI version from `packages/kilo-jetbrains/package.json`, not from the JetBrains plugin version.
|
||||
|
||||
Verify repo CLI dev mode is disabled on `main` before creating the immutable tag:
|
||||
|
||||
```bash
|
||||
git show origin/main:packages/kilo-jetbrains/gradle.properties | grep '^kilo.cli.pinned=' || true
|
||||
```
|
||||
|
||||
If `kilo.cli.pinned` is present and is not `true`, stop and ask the user to reset it to `true` on `main` before dispatching prepare. `kilo.cli.pinned=false` generates from and bundles the local repo CLI, so it is dev-only and non-releasable.
|
||||
|
||||
Read the pinned CLI version:
|
||||
|
||||
```bash
|
||||
@@ -67,7 +75,7 @@ kilo-windows-x64.zip
|
||||
|
||||
If the pin is stale or the release assets are missing, stop and ask the user to update `packages/kilo-jetbrains/package.json` on `main` before dispatching prepare. The prepare workflow tags `origin/main`, so the pin must already be reviewed and merged before the release tag is created.
|
||||
|
||||
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, pinned CLI version, and CLI release asset status to the user, then ask for confirmation before continuing.
|
||||
Show the resolved JetBrains plugin version, release kind, default `fromTagDefault`, `kilo.cli.pinned` status, pinned CLI version, and CLI release asset status to the user, then ask for confirmation before continuing.
|
||||
|
||||
## Prepare Workflow
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- `plugin.xml` `<content>` entries ↔ module XML descriptors (`kilo.jetbrains.{shared,frontend,backend}.xml`)
|
||||
- Service classes ↔ `<applicationService>`/`<projectService>` entries in the corresponding module XML
|
||||
- `packages/kilo-jetbrains/package.json` version ↔ GitHub CLI release tag consumed by the backend downloader
|
||||
- `packages/kilo-jetbrains/gradle.properties` `kilo.cli.pinned` ↔ Gradle and release-script gates
|
||||
|
||||
## IntelliJ Platform Source Lookup
|
||||
|
||||
@@ -154,8 +155,10 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
## CLI Integration
|
||||
|
||||
- CLI process spawning, download, extraction, and lifecycle belong in `backend`.
|
||||
- The plugin does not bundle CLI binaries. At connect time the backend downloads the GitHub Release asset for the version pinned in `packages/kilo-jetbrains/package.json`; `backend` resources include `kilo.properties` with `cli.version` for split-mode RPC and runtime use.
|
||||
- The generated API client is produced from the pinned release binary by running `kilo generate` during the Gradle OpenAPI generation task.
|
||||
- By default, the plugin does not bundle CLI binaries. At connect time the backend downloads the GitHub Release asset for the version pinned in `packages/kilo-jetbrains/package.json`; `backend` resources include `kilo.properties` with `cli.version` and `cli.pinned` for split-mode RPC and runtime use.
|
||||
- `kilo.cli.pinned=false` in `gradle.properties` is dev-only repo CLI mode: OpenAPI generation runs `bun run --conditions=browser ./src/index.ts generate` from `packages/opencode/`, and runtime extracts a staged local CLI resource instead of downloading.
|
||||
- Repo CLI mode requires a local CLI build. Run `./gradlew :backend:buildRepoCli` from `packages/kilo-jetbrains/` or `bun run script/build.ts --single --skip-install` from `packages/opencode/`, then let `:backend:stageRepoCli` bundle the full `dist/@kilocode/cli-<os>-<arch>/bin/` directory.
|
||||
- Production builds must keep `kilo.cli.pinned=true`; Gradle release mode, release scripts, and `script/build-version.sh` reject repo CLI mode.
|
||||
- For OS and environment checks, prefer IntelliJ Platform classes over raw JVM APIs such as `System.getProperty(...)` or `System.getenv(...)`.
|
||||
- Detect architecture with `com.intellij.util.system.CpuArch.CURRENT`, not `System.getProperty("os.arch")`.
|
||||
- Detect OS with `com.intellij.openapi.util.SystemInfo.isMac` / `isLinux` / `isWindows`.
|
||||
@@ -191,7 +194,8 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
|
||||
- **Marketplace version build**: Use `script/build-version.sh <version>` from `packages/kilo-jetbrains/` to clean, build, sign, and verify the JetBrains Marketplace plugin ZIP. Pass `--skip-verification` only when explicitly needed.
|
||||
- **Test version build**: If the user asks for a JetBrains test build, still require a version and use `script/build-version.sh <version> --skip-signing --skip-verification` from `packages/kilo-jetbrains/` so no signing secrets are needed. Add `--skip-clean` only when the user wants a faster incremental test build.
|
||||
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. A cold build downloads the pinned CLI release via `generateOpenApiSpec` and needs network access; Gradle-cached incremental runs skip the download. It does not bundle per-platform CLI binaries.
|
||||
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. A cold pinned build downloads the pinned CLI release via `generateOpenApiSpec` and needs network access; Gradle-cached incremental runs skip the download. Repo CLI mode (`-Pkilo.cli.pinned=false`) generates the spec from local source and bundles the staged local CLI binary.
|
||||
- **Build local repo CLI for JetBrains dev**: `./gradlew :backend:buildRepoCli` from `packages/kilo-jetbrains/` builds `packages/opencode/dist/@kilocode/cli-<os>-<arch>/bin/`. `stageRepoCli` intentionally does not depend on this task; missing binaries fail with instructions instead of silently starting a slow CLI build.
|
||||
- **Full build**: `bun run build` from `packages/kilo-jetbrains/` (runs Gradle `buildPlugin`).
|
||||
- **Gradle only**: `./gradlew buildPlugin` from `packages/kilo-jetbrains/`.
|
||||
- **Java checks**: Do not run `java -version` as a routine preflight. Gradle commands already fail clearly when Java is missing or incompatible; check Java only when diagnosing that failure mode.
|
||||
@@ -202,7 +206,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
|
||||
### CLI/SDK Change Awareness
|
||||
|
||||
- JetBrains runtime behavior depends on the downloaded CLI release pinned by `packages/kilo-jetbrains/package.json`; local `packages/opencode/` changes are not used unless published and pinned.
|
||||
- JetBrains runtime behavior normally depends on the downloaded CLI release pinned by `packages/kilo-jetbrains/package.json`; local `packages/opencode/` changes are used only with `kilo.cli.pinned=false` repo CLI mode.
|
||||
- If there are relevant server/API changes outside `packages/kilo-jetbrains/`, warn the user that JetBrains may need a newly published/pinned CLI release and regenerated SDK artifacts.
|
||||
|
||||
## UI Guidelines
|
||||
|
||||
@@ -10,6 +10,8 @@ Maintainers can use the Kilo `release-jetbrains` skill to drive this process fro
|
||||
|
||||
JetBrains plugin builds and runtime downloads use the Kilo Core version pinned in `packages/kilo-jetbrains/package.json`, so verify that pin points at a published `v<version>` release before creating the release tag.
|
||||
|
||||
`kilo.cli.pinned=false` in `packages/kilo-jetbrains/gradle.properties` is local development mode only. It generates the client from `packages/opencode/` and bundles a locally built CLI into the plugin; production Gradle builds and release scripts fail until the property is restored to `true`.
|
||||
|
||||
The skill lives at `.kilo/skills/release-jetbrains/SKILL.md`. It does not move or recreate release tags, and merge permission is only required if the user explicitly asks the skill to merge the release PR automatically.
|
||||
|
||||
## Create Release Tag And PR
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import normalization.NormalizeOpenApiSpecTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.tasks.Exec
|
||||
import org.gradle.api.tasks.WriteProperties
|
||||
|
||||
plugins {
|
||||
@@ -17,6 +19,10 @@ val generatedApi = layout.buildDirectory.dir("generated/openapi/src/main/kotlin"
|
||||
val rawSpec = layout.buildDirectory.file("generated/openapi-spec/openapi.raw.json")
|
||||
val generatedSpec = layout.buildDirectory.file("generated/openapi-spec/openapi.json")
|
||||
val generatedProps = layout.buildDirectory.dir("generated/kilo-props")
|
||||
val generatedCli = layout.buildDirectory.dir("generated/kilo-cli-res")
|
||||
val pinned = providers.gradleProperty("kilo.cli.pinned").map { it.trim().toBoolean() }.orElse(true)
|
||||
val repoCli = pinned.map { !it }
|
||||
val repoRootDir = rootProject.layout.projectDirectory.dir("../opencode")
|
||||
|
||||
val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirectory.file("package.json")).asText.map { text ->
|
||||
Regex("\"version\"\\s*:\\s*\"([^\"]+)\"").find(text)?.groupValues?.get(1)
|
||||
@@ -26,6 +32,7 @@ val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirector
|
||||
sourceSets {
|
||||
main {
|
||||
resources.srcDir(generatedProps)
|
||||
if (repoCli.get()) resources.srcDir(generatedCli)
|
||||
kotlin.srcDir(generatedApi)
|
||||
}
|
||||
}
|
||||
@@ -35,11 +42,14 @@ val writeKiloProperties by tasks.registering(WriteProperties::class) {
|
||||
val out = generatedProps.map { it.file("kilo.properties") }
|
||||
destinationFile.set(out)
|
||||
property("cli.version", pinnedCliVersion)
|
||||
property("cli.pinned", pinned.map { it.toString() })
|
||||
}
|
||||
|
||||
val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) {
|
||||
description = "Generate CLI OpenAPI spec into the build directory"
|
||||
cliVersion.set(pinnedCliVersion)
|
||||
repo.set(repoCli)
|
||||
repoRoot.set(repoRootDir)
|
||||
token.set(
|
||||
providers.environmentVariable("GH_TOKEN")
|
||||
.orElse(providers.environmentVariable("GITHUB_TOKEN"))
|
||||
@@ -48,6 +58,36 @@ val generateOpenApiSpec by tasks.registering(GenerateOpenApiSpecTask::class) {
|
||||
spec.set(rawSpec)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
fun platform(): String {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
val name = when {
|
||||
os.contains("mac") || os.contains("darwin") -> "darwin"
|
||||
os.contains("linux") -> "linux"
|
||||
os.contains("windows") -> "windows"
|
||||
else -> throw GradleException("Unsupported OS: ${System.getProperty("os.name")}")
|
||||
}
|
||||
val arch = when (System.getProperty("os.arch").lowercase()) {
|
||||
"aarch64", "arm64" -> "arm64"
|
||||
"x86_64", "amd64" -> "x64"
|
||||
else -> throw GradleException("Unsupported architecture: ${System.getProperty("os.arch")}")
|
||||
}
|
||||
return "$name-$arch"
|
||||
}
|
||||
|
||||
val stageRepoCli by tasks.registering(StageRepoCliTask::class) {
|
||||
description = "Stage the local repo CLI into backend resources"
|
||||
val bin = repoRootDir.dir("dist/@kilocode/cli-${platform()}/bin")
|
||||
this.bin.set(bin)
|
||||
archive.set(generatedCli.map { it.file("kilo-cli.zip") })
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
val normalizeOpenApiSpec by tasks.registering(NormalizeOpenApiSpecTask::class) {
|
||||
description = "Normalize upstream CLI OpenAPI metadata before Kotlin client generation"
|
||||
dependsOn(generateOpenApiSpec)
|
||||
@@ -102,11 +142,13 @@ val fixGeneratedApi by tasks.registering(FixGeneratedApiTask::class) {
|
||||
|
||||
tasks.named("compileKotlin") {
|
||||
dependsOn(fixGeneratedApi, writeKiloProperties)
|
||||
if (repoCli.get()) dependsOn(stageRepoCli)
|
||||
inputs.dir(generatedApi)
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(writeKiloProperties)
|
||||
if (repoCli.get()) dependsOn(stageRepoCli)
|
||||
}
|
||||
|
||||
tasks.named("compileTestKotlin") {
|
||||
|
||||
+5
@@ -87,6 +87,11 @@ class KiloBackendCliManager(
|
||||
private suspend fun resolveCli(onProgress: (CliDownload) -> Unit): File {
|
||||
val force = forceExtract
|
||||
forceExtract = false
|
||||
if (!KiloProps.pinned()) {
|
||||
if (force) log.info("Force re-extracting local repo CLI ${KiloProps.cliVersion()}")
|
||||
onProgress(CliDownload(100, KiloProps.cliVersion(), KiloCliPlatform.current()))
|
||||
return KiloRepoCli.extract(force)
|
||||
}
|
||||
if (force) log.info("Force re-downloading CLI ${KiloProps.cliVersion()}")
|
||||
return KiloCliDownloader(log = log).resolve(KiloProps.cliVersion(), force, onProgress)
|
||||
}
|
||||
|
||||
@@ -13,4 +13,8 @@ object KiloProps {
|
||||
|
||||
fun cliVersion(): String = props.getProperty("cli.version")
|
||||
?: throw IllegalStateException("cli.version missing from kilo.properties")
|
||||
|
||||
fun pinned(): Boolean = pinned(props)
|
||||
|
||||
internal fun pinned(props: Properties): Boolean = props.getProperty("cli.pinned")?.toBoolean() ?: true
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
object KiloRepoCli {
|
||||
fun extract(force: Boolean): File = extract(
|
||||
force = force,
|
||||
root = File(PathManager.getSystemPath(), "kilo/repo-cli"),
|
||||
source = {
|
||||
KiloRepoCli::class.java.classLoader.getResourceAsStream("kilo-cli.zip")
|
||||
?: throw IllegalStateException("kilo-cli.zip resource not found; rebuild with kilo.cli.pinned=false")
|
||||
},
|
||||
)
|
||||
|
||||
internal fun extract(force: Boolean, root: File, source: () -> InputStream): File {
|
||||
val exe = File(root, "bin/${KiloCliPlatform.exe()}")
|
||||
val done = File(root, ".complete")
|
||||
if (!force && done.isFile && exe.isFile) {
|
||||
if (!SystemInfo.isWindows) exe.setExecutable(true)
|
||||
return exe
|
||||
}
|
||||
|
||||
if (root.exists() && !root.deleteRecursively()) {
|
||||
throw IllegalStateException("Failed to delete local repo CLI under ${root.absolutePath}")
|
||||
}
|
||||
if (!root.isDirectory && !root.mkdirs()) {
|
||||
throw IllegalStateException("Failed to create local repo CLI directory ${root.absolutePath}")
|
||||
}
|
||||
|
||||
source().use { input ->
|
||||
ZipInputStream(input.buffered()).use { zip ->
|
||||
while (true) {
|
||||
val entry = zip.nextEntry ?: break
|
||||
write(root, entry.name, entry.isDirectory) { out -> zip.copyTo(out) }
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!exe.isFile) throw IllegalStateException("Local repo CLI archive did not contain bin/${KiloCliPlatform.exe()}")
|
||||
if (!SystemInfo.isWindows) exe.setExecutable(true)
|
||||
done.writeText("ok\n")
|
||||
return exe
|
||||
}
|
||||
|
||||
private fun write(dir: File, name: String, directory: Boolean, copy: (OutputStream) -> Unit) {
|
||||
val path = if (name.startsWith("bin/")) name else "bin/$name"
|
||||
val target = File(dir, path).canonicalFile
|
||||
val base = dir.canonicalFile
|
||||
if (target != base && !target.path.startsWith(base.path + File.separator)) {
|
||||
throw IllegalStateException("Archive entry escapes target directory: $name")
|
||||
}
|
||||
if (directory) {
|
||||
target.mkdirs()
|
||||
return
|
||||
}
|
||||
target.parentFile.mkdirs()
|
||||
target.outputStream().use(copy)
|
||||
if (!SystemInfo.isWindows && (target.name == "kilo" || target.name == "bwrap")) {
|
||||
target.setExecutable(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class KiloRepoCliTest {
|
||||
@TempDir
|
||||
lateinit var dir: File
|
||||
|
||||
@Test
|
||||
fun `extracts cached repo cli and force re-extracts`() {
|
||||
val first = archive("#!/bin/old\n")
|
||||
val next = archive("#!/bin/new\n")
|
||||
val cli = KiloRepoCli.extract(false, dir) { ByteArrayInputStream(first) }
|
||||
|
||||
assertTrue(cli.isFile)
|
||||
assertEquals("#!/bin/old\n", cli.readText())
|
||||
assertTrue(File(cli.parentFile, "kilo-sandbox-mutation-worker.js").isFile)
|
||||
assertTrue(File(dir, ".complete").isFile)
|
||||
|
||||
val cached = KiloRepoCli.extract(false, dir) { ByteArrayInputStream(next) }
|
||||
assertEquals(cli.absolutePath, cached.absolutePath)
|
||||
assertEquals("#!/bin/old\n", cached.readText())
|
||||
|
||||
val forced = KiloRepoCli.extract(true, dir) { ByteArrayInputStream(next) }
|
||||
assertEquals(cli.absolutePath, forced.absolutePath)
|
||||
assertEquals("#!/bin/new\n", forced.readText())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects archive entries that escape root`() {
|
||||
val ex = assertFailsWith<IllegalStateException> {
|
||||
KiloRepoCli.extract(false, dir) { ByteArrayInputStream(archive(entry = "../../../bad")) }
|
||||
}
|
||||
|
||||
assertContains(ex.message.orEmpty(), "escapes target directory")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pinned defaults true unless explicitly false`() {
|
||||
assertEquals(true, KiloProps.pinned(Properties()))
|
||||
assertEquals(true, KiloProps.pinned(Properties().apply { setProperty("cli.pinned", "true") }))
|
||||
assertEquals(false, KiloProps.pinned(Properties().apply { setProperty("cli.pinned", "false") }))
|
||||
}
|
||||
|
||||
private fun archive(script: String = "#!/bin/sh\n", entry: String = "bin/${KiloCliPlatform.exe()}"): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
ZipOutputStream(out).use { zip ->
|
||||
zip.putNextEntry(ZipEntry(entry))
|
||||
zip.write(script.toByteArray())
|
||||
zip.closeEntry()
|
||||
zip.putNextEntry(ZipEntry("bin/kilo-sandbox-mutation-worker.js"))
|
||||
zip.write("worker".toByteArray())
|
||||
zip.closeEntry()
|
||||
}
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
@get:Input
|
||||
abstract val cliVersion: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val repo: Property<Boolean>
|
||||
|
||||
@get:Internal
|
||||
abstract val repoRoot: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val token: Property<String>
|
||||
|
||||
@@ -49,20 +55,51 @@ abstract class GenerateOpenApiSpecTask : DefaultTask() {
|
||||
@get:Inject
|
||||
abstract val exec: ExecOperations
|
||||
|
||||
init {
|
||||
repo.convention(false)
|
||||
outputs.upToDateWhen { !repo.getOrElse(false) }
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
if (repo.getOrElse(false)) {
|
||||
generateFromRepo()
|
||||
return
|
||||
}
|
||||
val kilo = resolve()
|
||||
generate(kilo.absolutePath)
|
||||
}
|
||||
|
||||
private fun generateFromRepo() {
|
||||
val root = repoRoot.asFile.get()
|
||||
val out = ByteArrayOutputStream()
|
||||
val err = ByteArrayOutputStream()
|
||||
val result = exec.exec {
|
||||
commandLine(kilo.absolutePath, "generate")
|
||||
workingDir = root
|
||||
commandLine("bun", "run", "--conditions=browser", "./src/index.ts", "generate")
|
||||
standardOutput = out
|
||||
errorOutput = err
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
if (result.exitValue != 0) {
|
||||
writeSpec(result.exitValue, out, err)
|
||||
}
|
||||
|
||||
private fun generate(kilo: String) {
|
||||
val out = ByteArrayOutputStream()
|
||||
val err = ByteArrayOutputStream()
|
||||
val result = exec.exec {
|
||||
commandLine(kilo, "generate")
|
||||
standardOutput = out
|
||||
errorOutput = err
|
||||
isIgnoreExitValue = true
|
||||
}
|
||||
writeSpec(result.exitValue, out, err)
|
||||
}
|
||||
|
||||
private fun writeSpec(code: Int, out: ByteArrayOutputStream, err: ByteArrayOutputStream) {
|
||||
if (code != 0) {
|
||||
throw GradleException(
|
||||
"kilo generate failed with exit code ${result.exitValue}.\n" +
|
||||
"kilo generate failed with exit code $code.\n" +
|
||||
err.toString(Charsets.UTF_8).take(2000)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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.Internal
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
abstract class StageRepoCliTask : DefaultTask() {
|
||||
@get:Internal
|
||||
abstract val bin: DirectoryProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val archive: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val dir = bin.asFile.get()
|
||||
val exe = File(dir, exe())
|
||||
if (!exe.isFile) {
|
||||
throw GradleException(
|
||||
"Repo CLI binary not found at ${exe.absolutePath}. Run ./gradlew :backend:buildRepoCli " +
|
||||
"(or bun run script/build.ts --single --skip-install in packages/opencode) first."
|
||||
)
|
||||
}
|
||||
|
||||
val out = archive.get().asFile
|
||||
out.parentFile.mkdirs()
|
||||
ZipOutputStream(out.outputStream().buffered()).use { zip ->
|
||||
dir.walkTopDown()
|
||||
.filter { it.isFile }
|
||||
.forEach { file ->
|
||||
val name = "bin/${file.relativeTo(dir).invariantSeparatorsPath}"
|
||||
zip.putNextEntry(ZipEntry(name))
|
||||
file.inputStream().use { it.copyTo(zip) }
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exe() = if (System.getProperty("os.name").lowercase().contains("windows")) "kilo.exe" else "kilo"
|
||||
}
|
||||
@@ -82,6 +82,7 @@ fun gitTag(): String? {
|
||||
}
|
||||
|
||||
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
|
||||
val pinned = providers.gradleProperty("kilo.cli.pinned").map { it.trim().toBoolean() }.orElse(true).get()
|
||||
val override = providers.gradleProperty("kilo.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val prop = providers.gradleProperty("kilo.jetbrains.version").orNull?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val tag = gitTag()?.removePrefix("jetbrains/v")
|
||||
@@ -89,6 +90,10 @@ val ver = override?.let(::checked) ?: prop?.let(::checked) ?: if (release) check
|
||||
tag ?: error("Missing JetBrains plugin version. Publish builds must set kilo.jetbrains.version or run from a jetbrains/v<version> tag."),
|
||||
) else checked(tag ?: "0.0.0-dev")
|
||||
|
||||
if (release && !pinned) error(
|
||||
"kilo.cli.pinned=false is a dev-only mode and cannot be released. Set kilo.cli.pinned=true before a production/publish build."
|
||||
)
|
||||
|
||||
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").map(::port).orElse(0)
|
||||
val isolated = providers.gradleProperty("kilo.dev.storage.isolated").map { it.toBoolean() }.orElse(false)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
kotlin.stdlib.default.dependency=false
|
||||
kilo.jetbrains.version=7.0.2
|
||||
# When true (default) the JetBrains plugin uses the pinned CLI release from package.json.
|
||||
# Set to false ONLY for local dev: generate the client from local source + bundle the local binary.
|
||||
# false is NOT releasable -- production builds fail unless this is true.
|
||||
kilo.cli.pinned=true
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
@@ -86,6 +86,11 @@ if [[ ! -d "$plugin" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q '^kilo\.cli\.pinned=false[[:space:]]*$' "$plugin/gradle.properties"; then
|
||||
echo "kilo.cli.pinned=false is a dev-only mode and cannot be released. Set kilo.cli.pinned=true before building a version." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$sign" == "1" ]]; then
|
||||
for file in "$chain" "$key" "$pass"; do
|
||||
if [[ ! -s "$file" ]]; then
|
||||
|
||||
@@ -42,6 +42,9 @@ if (kind === "stable" && !/^\d+\.\d+\.\d+$/.test(ver)) throw new Error("Stable v
|
||||
if (!semver.valid(ver)) throw new Error(`Invalid semver: ${ver}`)
|
||||
|
||||
await $`git fetch origin main --tags`
|
||||
if (!(await pinned())) {
|
||||
throw new Error("packages/kilo-jetbrains/gradle.properties has kilo.cli.pinned=false; JetBrains releases require kilo.cli.pinned=true")
|
||||
}
|
||||
|
||||
const tag = `jetbrains/v${ver}`
|
||||
const branch = `jetbrains/release/v${ver}`
|
||||
@@ -214,6 +217,13 @@ async function writeprops(ver: string) {
|
||||
await Bun.write(props, next.endsWith("\n") ? next : `${next}\n`)
|
||||
}
|
||||
|
||||
async function pinned() {
|
||||
const text = await Bun.file(props).text()
|
||||
const line = text.split(/\r?\n/).find((item) => item.startsWith("kilo.cli.pinned="))
|
||||
const value = line?.split("=", 2)[1]?.trim().toLowerCase()
|
||||
return value !== "false"
|
||||
}
|
||||
|
||||
async function writelog(ver: string, entry: string) {
|
||||
const current = await Bun.file(log)
|
||||
.text()
|
||||
|
||||
@@ -73,6 +73,9 @@ if (sha !== commit) throw new Error(`${tag} points at ${sha}, expected ${commit}
|
||||
const prop = await props()
|
||||
if (prop !== ver)
|
||||
throw new Error(`packages/kilo-jetbrains/gradle.properties kilo.jetbrains.version is ${prop}, expected ${ver}`)
|
||||
if (!(await pinned())) {
|
||||
throw new Error("packages/kilo-jetbrains/gradle.properties has kilo.cli.pinned=false; JetBrains releases require kilo.cli.pinned=true")
|
||||
}
|
||||
|
||||
const changelog = await Bun.file("packages/kilo-jetbrains/CHANGELOG.md").text()
|
||||
if (!changelog.includes(`## [${ver}]`)) throw new Error(`CHANGELOG.md is missing section for ${ver}`)
|
||||
@@ -117,3 +120,10 @@ async function props() {
|
||||
if (!value) throw new Error("packages/kilo-jetbrains/gradle.properties is missing kilo.jetbrains.version")
|
||||
return value
|
||||
}
|
||||
|
||||
async function pinned() {
|
||||
const text = await Bun.file("packages/kilo-jetbrains/gradle.properties").text()
|
||||
const line = text.split(/\r?\n/).find((item) => item.startsWith("kilo.cli.pinned="))
|
||||
const value = line?.split("=", 2)[1]?.trim().toLowerCase()
|
||||
return value !== "false"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user