mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(jetbrains): isolate dev CLI storage under .kilo-dev via XDG env vars
This commit is contained in:
@@ -37,6 +37,7 @@ target
|
||||
|
||||
# Local dev files
|
||||
opencode-dev
|
||||
.kilo-dev/
|
||||
UPCOMING_CHANGELOG.md
|
||||
logs/
|
||||
*.bun-build
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/kilo-jetbrains" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="-Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=0" />
|
||||
<option name="scriptParameters" value="-Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=0 -Pkilo.dev.storage.isolated=true" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
|
||||
@@ -153,9 +153,17 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi
|
||||
|
||||
- The plugin spawns `kilo serve --port 0` (OS assigns random port) and reads stdout for `listening on http://...:(\d+)` to discover the port.
|
||||
- A random 32-byte hex password is passed via `KILO_SERVER_PASSWORD` env var for Basic Auth.
|
||||
- Key env vars: `KILO_CLIENT=jetbrains`, `KILO_PLATFORM=jetbrains`, `KILO_APP_NAME=kilo-code`, `KILO_ENABLE_QUESTION_TOOL=true`.
|
||||
- Fixed env vars set on every spawn: `KILO_CLIENT=jetbrains`, `KILO_PLATFORM=jetbrains`, `KILO_APP_NAME=kilo-code`, `KILO_ENABLE_QUESTION_TOOL=true`, `KILO_DISABLE_CLAUDE_CODE=true`, `KILOCODE_FEATURE=jetbrains-plugin`.
|
||||
- This is the same protocol used by the VS Code extension (`packages/kilo-vscode/src/services/cli-backend/server-manager.ts`).
|
||||
|
||||
### Dev Storage Isolation
|
||||
|
||||
- In development (`runIdeBackend` / `runIde`), the Gradle property `kilo.dev.storage.isolated=true` makes the backend set `XDG_DATA_HOME`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, and `XDG_CACHE_HOME` to `<worktree>/.kilo-dev/{data,config,state,cache}` before spawning the CLI. The worktree root comes from the `kilo.dev.worktree.root` JVM system property (auto-set by Gradle from the project directory).
|
||||
- The checked-in `Run IDE (Backend)` run configuration enables isolation by default (`-Pkilo.dev.storage.isolated=true`). Developers can disable it by passing `-Pkilo.dev.storage.isolated=false`.
|
||||
- Use standard `XDG_*_HOME` env vars for this isolation. Do not introduce custom `KILO_DATA_DIR`, `KILO_GLOBAL_CONFIG_DIR`, `KILO_STATE_DIR`, or `KILO_CACHE_DIR` env vars — the CLI core already respects `XDG_*_HOME` via `xdg-basedir`.
|
||||
- The `.kilo-dev/` directory is gitignored and created automatically on first run.
|
||||
- The implementation lives in `KiloBackendCliManager.buildEnv()` / `devStorageEnv()`. Tests: `KiloBackendCliManagerEnvTest`.
|
||||
|
||||
## Build and Verification
|
||||
|
||||
- **Typecheck**: `bun run typecheck` or `./gradlew typecheck` from `packages/kilo-jetbrains/` — compiles all Kotlin sources including the generated API client. Does NOT require CLI binaries.
|
||||
|
||||
@@ -72,7 +72,7 @@ See [RELEASING.md](RELEASING.md) for the full release process, including how to
|
||||
|
||||
## Run the plugin
|
||||
|
||||
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.
|
||||
Use the `runIde` Gradle task (available in the Gradle tool window or via `./gradlew runIde` from `packages/kilo-jetbrains/`) to launch a sandboxed IntelliJ instance with the plugin installed.
|
||||
|
||||
`runIde` does not prepare the CLI binary automatically. Run `bun run build --prepare-cli` from `packages/kilo-jetbrains/` first to copy the local-platform binary into `backend/build/generated/cli/cli/`.
|
||||
|
||||
@@ -80,18 +80,45 @@ Production packaging still requires running `bun run build:production` so all pl
|
||||
|
||||
### Run the split backend
|
||||
|
||||
The `Run IDE (Backend)` / `runIdeBackend` path prepares the local-platform CLI binary automatically when `backend/build/generated/cli/cli/` does not contain the expected binary. It runs `bun run build --prepare-cli` and then copies backend resources for the sandbox.
|
||||
Use the checked-in `Run IDE (Backend)` run configuration (or `./gradlew runIdeBackend`) to launch just the backend half of a split-mode session. It prepares the local-platform CLI binary automatically when `backend/build/generated/cli/cli/` does not contain the expected binary.
|
||||
|
||||
The backend run configuration includes `-Pkilo.splitModeServerPort=0` by default. Leave it blank, set it to `0`, or omit it to use a random high port from `49152..65535`; set it to a fixed port when you need one:
|
||||
Use `Run IDE (Split Mode)` to launch both halves at once (composes `Run IDE (Backend)` + `Run IDE (Frontend)`).
|
||||
|
||||
### Backend Gradle properties
|
||||
|
||||
All properties below are passed with `-P` on the Gradle command line or in the run configuration's script parameters field.
|
||||
|
||||
| Property | Default | Description |
|
||||
|---|---|---|
|
||||
| `kilo.splitModeServerPort` | random high port | Backend split-mode server port. `0` or omitted picks a random port from 49152-65535. |
|
||||
| `kilo.dev.storage.isolated` | `false` | When `true`, CLI runs with `XDG_*_HOME` pointing to `.kilo-dev/` in the worktree root, fully isolating dev storage from your real Kilo installation. Enabled by default in `Run IDE (Backend)`. |
|
||||
| `kilo.dev.worktree.root` | monorepo root | Worktree root used to resolve `.kilo-dev/`. Auto-detected from the Gradle project directory; override only when the auto-detection is wrong. |
|
||||
| `kilo.bun.path` | `bun` on `$PATH` | Absolute path to Bun. Set this when IntelliJ-launched Gradle cannot find Bun automatically. |
|
||||
|
||||
Example with a fixed split-mode port:
|
||||
|
||||
```text
|
||||
-Pkilo.dev.log.level=debug -Pkilo.splitModeServerPort=12345
|
||||
```
|
||||
|
||||
If IntelliJ-launched Gradle cannot find Bun automatically, add this to the backend Gradle run configuration arguments:
|
||||
### Dev storage isolation
|
||||
|
||||
When `kilo.dev.storage.isolated=true`, the CLI subprocess receives standard `XDG_*_HOME` env vars pointing under `.kilo-dev/` in the worktree root:
|
||||
|
||||
```
|
||||
.kilo-dev/
|
||||
data/ -> XDG_DATA_HOME (CLI uses .../data/kilo for sessions, logs, ...)
|
||||
config/ -> XDG_CONFIG_HOME (CLI uses .../config/kilo for global config)
|
||||
state/ -> XDG_STATE_HOME (CLI uses .../state/kilo for state)
|
||||
cache/ -> XDG_CACHE_HOME (CLI uses .../cache/kilo for cache, bin)
|
||||
```
|
||||
|
||||
This keeps all development data isolated from your real Kilo installation. The `.kilo-dev/` directory is gitignored and created automatically on first run.
|
||||
|
||||
The `Run IDE (Backend)` run configuration enables this by default. To disable it:
|
||||
|
||||
```text
|
||||
-Pkilo.bun.path=/absolute/path/to/bun
|
||||
-Pkilo.dev.storage.isolated=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+36
-11
@@ -121,21 +121,46 @@ class KiloBackendCliManager(
|
||||
return target
|
||||
}
|
||||
|
||||
// Must be called from a background thread — devStorageEnv() performs blocking I/O (mkdirs).
|
||||
internal fun buildEnv(pwd: String, base: Map<String, String> = System.getenv()): Map<String, String> = buildMap {
|
||||
putAll(base)
|
||||
put("KILO_SERVER_PASSWORD", pwd)
|
||||
put("KILO_CLIENT", "jetbrains")
|
||||
put("KILO_ENABLE_QUESTION_TOOL", "true")
|
||||
put("KILO_PLATFORM", "jetbrains")
|
||||
put("KILO_APP_NAME", "kilo-code")
|
||||
put("KILO_DISABLE_CLAUDE_CODE", "true")
|
||||
put("KILOCODE_FEATURE", "jetbrains-plugin")
|
||||
ideEnv().forEach { (k, v) -> put(k, v) }
|
||||
devStorageEnv()?.forEach { (k, v) -> put(k, v) }
|
||||
}
|
||||
|
||||
private fun devStorageEnv(): Map<String, String>? {
|
||||
val enabled = System.getProperty("kilo.dev.storage.isolated", "false").toBoolean()
|
||||
if (!enabled) return null
|
||||
val root = System.getProperty("kilo.dev.worktree.root") ?: run {
|
||||
log.warn("kilo.dev.storage.isolated=true but kilo.dev.worktree.root is not set; skipping dev storage isolation")
|
||||
return null
|
||||
}
|
||||
val dev = File(root, ".kilo-dev")
|
||||
val data = File(dev, "data").also { it.mkdirs() }
|
||||
val config = File(dev, "config").also { it.mkdirs() }
|
||||
val state = File(dev, "state").also { it.mkdirs() }
|
||||
val cache = File(dev, "cache").also { it.mkdirs() }
|
||||
log.info("Dev storage isolation enabled under ${dev.absolutePath}")
|
||||
return mapOf(
|
||||
"XDG_DATA_HOME" to data.absolutePath,
|
||||
"XDG_CONFIG_HOME" to config.absolutePath,
|
||||
"XDG_STATE_HOME" to state.absolutePath,
|
||||
"XDG_CACHE_HOME" to cache.absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun spawn(cli: File): CliServer.State =
|
||||
withContext(Dispatchers.IO) {
|
||||
val pwd = generatePassword()
|
||||
|
||||
val env = buildMap {
|
||||
putAll(System.getenv())
|
||||
put("KILO_SERVER_PASSWORD", pwd)
|
||||
put("KILO_CLIENT", "jetbrains")
|
||||
put("KILO_ENABLE_QUESTION_TOOL", "true")
|
||||
put("KILO_PLATFORM", "jetbrains")
|
||||
put("KILO_APP_NAME", "kilo-code")
|
||||
put("KILO_DISABLE_CLAUDE_CODE", "true")
|
||||
put("KILOCODE_FEATURE", "jetbrains-plugin")
|
||||
ideEnv().forEach { (k, v) -> put(k, v) }
|
||||
}
|
||||
val env = buildEnv(pwd)
|
||||
|
||||
val cmd = listOf(cli.absolutePath, "serve", "--port", "0")
|
||||
val builder = ProcessBuilder(cmd)
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
class KiloBackendCliManagerEnvTest {
|
||||
|
||||
private lateinit var tmp: File
|
||||
private val manager = KiloBackendCliManager()
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
tmp = Files.createTempDirectory("kilo-cli-env-test").toFile()
|
||||
System.clearProperty("kilo.dev.storage.isolated")
|
||||
System.clearProperty("kilo.dev.worktree.root")
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
System.clearProperty("kilo.dev.storage.isolated")
|
||||
System.clearProperty("kilo.dev.worktree.root")
|
||||
tmp.deleteRecursively()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation disabled - required JetBrains env vars are present`() {
|
||||
val env = manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
assertEquals("jetbrains", env["KILO_CLIENT"])
|
||||
assertEquals("true", env["KILO_ENABLE_QUESTION_TOOL"])
|
||||
assertEquals("jetbrains", env["KILO_PLATFORM"])
|
||||
assertEquals("kilo-code", env["KILO_APP_NAME"])
|
||||
assertEquals("true", env["KILO_DISABLE_CLAUDE_CODE"])
|
||||
assertEquals("jetbrains-plugin", env["KILOCODE_FEATURE"])
|
||||
assertEquals("pwd123", env["KILO_SERVER_PASSWORD"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation disabled - no XDG storage overrides are injected`() {
|
||||
val env = manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
assertFalse(env.containsKey("XDG_DATA_HOME"), "XDG_DATA_HOME should not be set when isolation is off")
|
||||
assertFalse(env.containsKey("XDG_CONFIG_HOME"), "XDG_CONFIG_HOME should not be set when isolation is off")
|
||||
assertFalse(env.containsKey("XDG_STATE_HOME"), "XDG_STATE_HOME should not be set when isolation is off")
|
||||
assertFalse(env.containsKey("XDG_CACHE_HOME"), "XDG_CACHE_HOME should not be set when isolation is off")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation enabled - XDG vars point under kilo-dev in worktree root`() {
|
||||
System.setProperty("kilo.dev.storage.isolated", "true")
|
||||
System.setProperty("kilo.dev.worktree.root", tmp.absolutePath)
|
||||
|
||||
val env = manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
val dev = File(tmp, ".kilo-dev")
|
||||
assertEquals(File(dev, "data").absolutePath, env["XDG_DATA_HOME"])
|
||||
assertEquals(File(dev, "config").absolutePath, env["XDG_CONFIG_HOME"])
|
||||
assertEquals(File(dev, "state").absolutePath, env["XDG_STATE_HOME"])
|
||||
assertEquals(File(dev, "cache").absolutePath, env["XDG_CACHE_HOME"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation enabled - kilo-dev subdirectories are created`() {
|
||||
System.setProperty("kilo.dev.storage.isolated", "true")
|
||||
System.setProperty("kilo.dev.worktree.root", tmp.absolutePath)
|
||||
|
||||
manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
val dev = File(tmp, ".kilo-dev")
|
||||
assertTrue(File(dev, "data").isDirectory, "data dir should be created")
|
||||
assertTrue(File(dev, "config").isDirectory, "config dir should be created")
|
||||
assertTrue(File(dev, "state").isDirectory, "state dir should be created")
|
||||
assertTrue(File(dev, "cache").isDirectory, "cache dir should be created")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation enabled - required JetBrains env vars are still present`() {
|
||||
System.setProperty("kilo.dev.storage.isolated", "true")
|
||||
System.setProperty("kilo.dev.worktree.root", tmp.absolutePath)
|
||||
|
||||
val env = manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
assertEquals("jetbrains", env["KILO_CLIENT"])
|
||||
assertEquals("pwd123", env["KILO_SERVER_PASSWORD"])
|
||||
assertEquals("jetbrains-plugin", env["KILOCODE_FEATURE"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation enabled - base env vars are preserved`() {
|
||||
System.setProperty("kilo.dev.storage.isolated", "true")
|
||||
System.setProperty("kilo.dev.worktree.root", tmp.absolutePath)
|
||||
|
||||
val env = manager.buildEnv("pwd123", mapOf("MY_CUSTOM_VAR" to "hello"))
|
||||
|
||||
assertEquals("hello", env["MY_CUSTOM_VAR"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isolation enabled without worktree root - no XDG vars are injected`() {
|
||||
System.setProperty("kilo.dev.storage.isolated", "true")
|
||||
// kilo.dev.worktree.root is intentionally not set
|
||||
|
||||
val env = manager.buildEnv("pwd123", emptyMap())
|
||||
|
||||
assertFalse(env.containsKey("XDG_DATA_HOME"), "XDG_DATA_HOME should not be set when root is missing")
|
||||
assertFalse(env.containsKey("XDG_CONFIG_HOME"), "XDG_CONFIG_HOME should not be set when root is missing")
|
||||
assertFalse(env.containsKey("XDG_STATE_HOME"), "XDG_STATE_HOME should not be set when root is missing")
|
||||
assertFalse(env.containsKey("XDG_CACHE_HOME"), "XDG_CACHE_HOME should not be set when root is missing")
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,10 @@ val ver = if (release) checked(
|
||||
val notes = providers.gradleProperty("kilo.changeNotes").orElse("Release candidate build.")
|
||||
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
|
||||
val splitPort = providers.gradleProperty("kilo.splitModeServerPort").map(::port).orElse(providers.provider(::fallback))
|
||||
val isolated = providers.gradleProperty("kilo.dev.storage.isolated").map { it.toBoolean() }.orElse(false)
|
||||
val worktreeRoot = providers.gradleProperty("kilo.dev.worktree.root").orElse(
|
||||
providers.provider { rootProject.layout.projectDirectory.asFile.parentFile.parentFile.canonicalPath }
|
||||
)
|
||||
|
||||
version = ver
|
||||
|
||||
@@ -184,6 +188,8 @@ tasks.withType<RunIdeTask> {
|
||||
systemProperty("kilo.dev.log.level", level)
|
||||
systemProperty("kilo.dev.log.chat.content", content)
|
||||
systemProperty("kilo.dev.log.chat.preview.max", preview)
|
||||
systemProperty("kilo.dev.storage.isolated", isolated.get().toString())
|
||||
systemProperty("kilo.dev.worktree.root", worktreeRoot.get())
|
||||
}
|
||||
|
||||
tasks.named<Delete>("clean") {
|
||||
|
||||
Reference in New Issue
Block a user