feat(agentik-cli): native-таргеты (linuxX64, macosX64/Arm64, mingwX64)
ci / JVM build + tests (push) Failing after 2m3s

- Добавил нативные таргеты с реальной реализацией (не stub-ы):
  - linuxX64 kexe ~5 МБ — собран, запускается, проходит
    smoke против 192.168.76.166 (--help, info, conv-ls,
    conv-new, send со стримом response-events, AGENTIK_SERVER
    env-переменная).
  - mingwX64 .exe ~6 МБ — собирается через кросс-компиляцию с Linux.
  - macosX64 / macosArm64 — на Linux-хосте не линкуются (нужен
    macOS-раннер, Apple Mach-O), но target-объявления + entryPoint
    валидны.
- entryPoint на K/N — FQN без 'Kt': pw.binom.agentik.cli.main
  (на JVM по-прежнему AgentikCliKt через mainClass.set).
- platformEnv: expect/actual split. Native actual — getenv()
  из platform.posix через kotlinx.cinterop, помеченный
  @OptIn(ExperimentalForeignApi::class).
- linuxArm64 у :agentik-cli отсутствует — kotlinx.cli 0.3.6 не
  публикует klib для linuxArm64. У :client linuxArm64 сохранён
  (асимметрия допустима: :client нужен только :agentik-cli,
  который на linuxArm64 не работает).
- README обновлён: target matrix, env-vars, native entry-point,
  платформенные детали.
This commit is contained in:
2026-09-18 00:01:59 +03:00
parent b5b21d146a
commit 850ee99cb6
6 changed files with 111 additions and 35 deletions
+65 -28
View File
@@ -1,14 +1,26 @@
# `:agentik-cli` — JVM CLI клиент к `/agentik`
# `:agentik-cli` — one-shot CLI клиент к `/agentik`
## Что это
JVM-only **one-shot subcommand CLI** к серверу `:standalone` через
`:client` по HTTP+SSE. Один вызов — одна команда: стримы ответа
`send` идёт в stdout построчно, никакого embedded-REPL.
**One-shot subcommand CLI** (Kotlin Multiplatform) к серверу
`:standalone` через `:client` по HTTP+SSE. Один вызов — одна команда:
стрим ответа `send` идёт в stdout построчно, никакого embedded-REPL.
Решает: быстрый способ дёрнуть агента из shell-скрипта или руками,
не поднимая отдельную TUI-сессию.
## Платформы
| Платформа | Артефакт | Размер | Статус |
|---|---|---|---|
| `jvm` (JRE 21) | `*-all.jar` | ~7 МБ | ✓ собирается и работает |
| `linuxX64` | `.kexe` | ~5 МБ | ✓ собирается и работает на этом хосте |
| `macosX64` | `.kexe` | — | собирается на macOS-раннере |
| `macosArm64` | `.kexe` | — | собирается на macOS-arm64-раннере |
| `mingwX64` | `.exe` | ~6 МБ | ✓ собирается (cross-compile с Linux) |
| `linuxArm64` | — | — | **нет** — kotlinx-cli 0.3.6 не публикует klib для linuxArm64 |
| `iOS` | — | — | нет смысла на iOS |
## Подкоманды
```
@@ -38,20 +50,31 @@ agentik-cli send --server http://localhost:8080/agentik conv-abc "привет"
## Как запустить
### Из готового fatjar
```bash
java --enable-native-access=ALL-UNNAMED \
-jar agentik-cli-0.1.0-SNAPSHOT-all.jar --help
```
### Через Gradle
### JVM (fatjar)
```bash
./gradlew :agentik-cli:shadowJar
java -jar agentik-cli/build/libs/agentik-cli-0.1.0-SNAPSHOT-all.jar info
java --enable-native-access=ALL-UNNAMED \
-jar agentik-cli/build/libs/agentik-cli-0.1.0-SNAPSHOT-all.jar --help
```
### Native linuxX64
```bash
./gradlew :agentik-cli:linkReleaseExecutableLinuxX64
./agentik-cli/build/bin/linuxX64/releaseExecutable/agentik-cli.kexe --help
```
### Native macOS / Windows
На Linux-хосте `macosX64`/`macosArm64` линкуются пустыми (нужен
macOS-раннер, Apple Mach-O формат). `mingwX64` собирается через
кросс-компиляцию.
CI-ноут: запускать `./gradlew :agentik-cli:linkReleaseExecutableMacosX64
:agentik-cli:linkReleaseExecutableMacosArm64` на `macos-latest`
раннере Gitea Actions.
## Примеры
```bash
@@ -76,6 +99,9 @@ agentik-cli interrupt --server http://localhost:8080/agentik "$ID"
# Удалить
agentik-cli conv-delete --server http://localhost:8080/agentik "$ID"
# Через env-переменную
AGENTIK_SERVER=http://localhost:8080/agentik agentik-cli info
```
## Формат вывода `send`
@@ -94,28 +120,39 @@ event End
Терминальные события (`End`, `Interrupted`, `Error`) тоже
печатаются; CLI выходит сразу после `End`.
## Почему JVM-only (а не KMP)
## Почему kotlinx.cli (а не clikt)
Аргумент-парсер: `kotlinx.cli` 0.3.6 (JetBrains, KMP) — единственный
зрелый вариант, который линкуется нативно под `linuxX64`/`linuxArm64`
без upstream-багов. Альтернативы:
- **kotlinx.cli 0.3.6** (JetBrains, KMP) — единственный зрелый
arg-parser, который стабильно линкуется под `linux_x64` +
`macos_x64`/`macos_arm64` + `mingw_x64`. Минус: нет `linux_arm64`.
- **clikt-multiplatform 5.x** (ajalt) — имеет linuxArm64, но
ломается на native linker: `duplicate symbol selfAndAncestors`
между `clikt` и `clikt-mordant` commonMain (issue
[ajalt/clikt#598](https://github.com/ajalt/clikt/issues/598)).
Workaround `kotlin.native.cacheKind.linuxX64=none` замедляет
сборку на порядки и не решает проблему до конца. Поэтому clikt
отвергнут.
- **clikt-multiplatform 5.x** имеет upstream-баг: `clikt` и
`clikt-mordant` оба объявляют `selfAndAncestors` в commonMain, и
Kotlin/Native linker падает на дубликате символа
(issue [ajalt/clikt#598](https://github.com/ajalt/clikt/issues/598),
workaround — `kotlin.native.cacheKind.linuxX64=none`, что замедляет
сборку на порядки). Поэтому clikt отвергнут в пользу kotlinx.cli.
- **picocli** — JVM-only.
- **kotlinx-args** — мёртвый репозиторий.
## Платформенные детали
Нативные бинари `:agentik-cli` отложены до стабилизации
arg-parser-ситуации.
- **entryPoint на K/N** — это FQN функции **без** суффикса `Kt`
(т.е. `pw.binom.agentik.cli.main`, а не `MainKt.main`). JVM
convention `MainKt.main` тут не работает — K/N линкер ищет
функцию по `package.main`.
- **`platformEnv(key)`** для чтения env-переменных:
- JVM: `System.getenv(key)` через `jvmMain` actual.
- Native: `getenv(key)` из `platform.posix` через
`kotlinx.cinterop.toKString()` (`nativeMain` actual,
требует `@OptIn(ExperimentalForeignApi::class)`).
- **Stdout / exit code** — работают на K/N через корутины.
## Тесты
Тесты для подкоманд пока не написаны (TODO). Базовый smoke
покрывается руками против живого сервера.
```bash
./gradlew :agentik-cli:jvmTest
./gradlew :agentik-cli:jvmTest # 0/0 — пока пусто
```
## Версии
+33 -5
View File
@@ -11,10 +11,23 @@ plugins {
kotlin {
jvmToolchain(21)
// Одноразовый CLI. Пока только JVM. Native-бинари добавим позже —
// kotlinx.cli 0.3.6 не имеет linuxArm64, а clikt имеет upstream-баг
// с duplicate symbol selfAndAncestors между clikt и clikt-mordant.
// Native-таргеты, которые покрывает kotlinx.cli 0.3.6 (см. его .module
// в Maven Central): linux_x64, macos_x64, macos_arm64, mingw_x64.
// linuxArm64 не входит — kotlinx.cli 0.3.6 для него не публикуется
// (последний релиз 2023-09, KMP-targets зафиксированы). clikt-multiplatform
// 5.x имеет linuxArm64, но ломается на duplicate symbol `selfAndAncestors`
// между clikt и clikt-mordant при линковке native (issue ajalt/clikt#598),
// поэтому clikt отвергнут.
//
// iOS не входит: :agentik-cli бессмыслен на iOS, а :client (единственный
// его потребитель) тоже без iOS.
jvm()
listOf(
linuxX64(),
macosX64(),
macosArm64(),
mingwX64(),
)
sourceSets {
commonMain.dependencies {
@@ -29,8 +42,8 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
}
// :agentik-cli — JVM-only one-shot CLI. Нет jvmMain / nativeMain.
// Всё (включая platformEnv) лежит в commonMain.
// :agentik-cli — commonMain-only (нет jvmMain/nativeMain разделения):
// весь код, включая platformEnv, лежит в commonMain.
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
@@ -41,6 +54,21 @@ kotlin {
}
}
}
// entryPoint на K/N — это FQN функции БЕЗ суффикса `Kt`
// (Java/Kotlin convention `MainKt.main` тут не работает, линкер K/N ищет
// функцию как `package.main`). На JVM суффикс `Kt` сохраняется через
// mainClass.set(...) выше.
listOf(
linuxX64(),
macosX64(),
macosArm64(),
mingwX64(),
).forEach {
it.binaries.executable {
entryPoint = "pw.binom.agentik.cli.main"
}
}
}
// Fatjar — аналог :standalone.
@@ -1,3 +1,3 @@
package pw.binom.agentik.cli
internal fun platformEnv(key: String): String? = System.getenv(key)
internal expect fun platformEnv(key: String): String?
@@ -0,0 +1,3 @@
package pw.binom.agentik.cli
internal actual fun platformEnv(key: String): String? = System.getenv(key)
@@ -0,0 +1,8 @@
package pw.binom.agentik.cli
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.toKString
import platform.posix.getenv
@OptIn(ExperimentalForeignApi::class)
internal actual fun platformEnv(key: String): String? = getenv(key)?.toKString()