epic-13: add cli and tui entry point, finish epic.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
id 'application'
|
||||
}
|
||||
|
||||
@@ -9,4 +10,10 @@ application {
|
||||
|
||||
dependencies {
|
||||
implementation "com.github.ajalt.clikt:clikt:5.0.1"
|
||||
|
||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-cio:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||
implementation "io.ktor:ktor-client-websockets:$ktor_version"
|
||||
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.correx.apps.cli
|
||||
|
||||
internal const val DEFAULT_PORT = 8080
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.correx.apps.cli
|
||||
|
||||
import com.correx.apps.cli.commands.ApproveCommand
|
||||
import com.correx.apps.cli.commands.ProviderCommand
|
||||
import com.correx.apps.cli.commands.RunCommand
|
||||
import com.correx.apps.cli.commands.SessionCommand
|
||||
import com.correx.apps.cli.commands.StatusCommand
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
|
||||
class CorrexCli : CliktCommand(name = "correx") {
|
||||
val json by option("--json", help = "Output machine-readable JSON").flag()
|
||||
val quiet by option("--quiet", help = "Output errors only").flag()
|
||||
|
||||
override fun run() = Unit
|
||||
}
|
||||
|
||||
fun buildCli(): CorrexCli = CorrexCli().subcommands(
|
||||
RunCommand(),
|
||||
SessionCommand(),
|
||||
ApproveCommand(),
|
||||
StatusCommand(),
|
||||
ProviderCommand(),
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.correx.apps.cli
|
||||
|
||||
fun main() {
|
||||
println("correx :: apps/cli")
|
||||
import com.github.ajalt.clikt.core.main
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
buildCli().main(args)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.required
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
private data class ApprovalRequest(
|
||||
val decision: String,
|
||||
val steeringNote: String?,
|
||||
)
|
||||
|
||||
class ApproveCommand : CliktCommand(name = "approve") {
|
||||
private val sessionId by argument("SESSION_ID")
|
||||
private val decision by option("--decision", help = "approve | reject | steer").required()
|
||||
private val note by option("--note", help = "Steering note")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
private val approveJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = (currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
|
||||
val decisionUpper = decision.uppercase()
|
||||
if (decisionUpper !in setOf("APPROVE", "REJECT", "STEER")) {
|
||||
System.err.println("Invalid decision: $decision. Must be approve, reject, or steer.")
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(approveJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
client.post("http://$host:$portInt/sessions/$sessionId/approve") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(ApprovalRequest(decision = decisionUpper, steeringNote = note))
|
||||
}
|
||||
if (outputJson) {
|
||||
println("{\"sessionId\": \"$sessionId\", \"decision\": \"$decisionUpper\"}")
|
||||
} else {
|
||||
println("Approval decision '$decisionUpper' sent for session $sessionId.")
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error sending approval: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class ProviderCommand : CliktCommand(name = "provider") {
|
||||
override fun run() = Unit
|
||||
|
||||
init {
|
||||
subcommands(ProviderListCommand())
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderListCommand : CliktCommand(name = "list") {
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
private val providerJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = (currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(providerJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val resp = client.get("http://$host:$portInt/providers")
|
||||
val body = resp.body<JsonArray>()
|
||||
if (outputJson) {
|
||||
println(body.toString())
|
||||
} else {
|
||||
body.forEach { el ->
|
||||
val obj = el as? JsonObject ?: return@forEach
|
||||
val providerId = obj["providerId"]?.jsonPrimitive?.content ?: ""
|
||||
val status = obj["status"]?.jsonPrimitive?.content ?: ""
|
||||
val load = obj["loadPercent"]?.jsonPrimitive?.content ?: "?"
|
||||
println("$providerId status=$status load=${load}%")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error fetching providers: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.correx.apps.cli.ws.CliWsClient
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.required
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
private data class StartSessionRequest(val workflowId: String, val sessionId: String?)
|
||||
|
||||
@Serializable
|
||||
private data class StartSessionResponse(val sessionId: String)
|
||||
|
||||
@Serializable
|
||||
private data class ApprovalResponsePayload(
|
||||
val type: String,
|
||||
val requestId: String,
|
||||
val decision: String,
|
||||
val steeringNote: String?,
|
||||
)
|
||||
|
||||
private data class RunContext(
|
||||
val sid: String,
|
||||
val outputJson: Boolean,
|
||||
val autoApprove: Boolean,
|
||||
val isTty: Boolean,
|
||||
)
|
||||
|
||||
private data class ApprovalContext(
|
||||
val session: WebSocketSession,
|
||||
val requestId: String,
|
||||
val toolName: String,
|
||||
val preview: String?,
|
||||
)
|
||||
|
||||
class RunCommand : CliktCommand(name = "run") {
|
||||
private val workflow by option("--workflow", help = "Path to workflow definition").required()
|
||||
private val sessionId by option("--session", help = "Existing session ID to resume")
|
||||
private val autoApprove by option("--auto-approve", help = "Auto-approve all approval requests").flag()
|
||||
private val host by option("--host", help = "Server host").default("localhost")
|
||||
private val port by option("--port", help = "Server port").default("$DEFAULT_PORT")
|
||||
|
||||
private val serializer = Json { ignoreUnknownKeys = true; classDiscriminator = "type" }
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = (currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(serializer) }
|
||||
install(WebSockets)
|
||||
}
|
||||
|
||||
val sid = startSession(client, host, portInt, outputJson) ?: run {
|
||||
client.close()
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
printLine("session_started", mapOf("sessionId" to sid, "workflow" to workflow), outputJson)
|
||||
|
||||
val ctx = RunContext(sid, outputJson, autoApprove, System.console() != null)
|
||||
var exitCode = 0
|
||||
val wsClient = CliWsClient(client)
|
||||
|
||||
wsClient.sessionStream(host, portInt, sid) { session, raw ->
|
||||
val msgJson = runCatching { serializer.parseToJsonElement(raw).jsonObject }.getOrNull()
|
||||
val keepGoing = handleMessage(session, msgJson, ctx) { exitCode = it }
|
||||
keepGoing
|
||||
}
|
||||
|
||||
client.close()
|
||||
if (exitCode != 0) {
|
||||
throw SystemExitException(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startSession(
|
||||
client: HttpClient,
|
||||
host: String,
|
||||
portInt: Int,
|
||||
outputJson: Boolean,
|
||||
): String? = runCatching {
|
||||
val resp = client.post("http://$host:$portInt/sessions") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(StartSessionRequest(workflowId = workflow, sessionId = sessionId))
|
||||
}
|
||||
resp.body<StartSessionResponse>().sessionId
|
||||
}.getOrElse { e ->
|
||||
printLine("error", mapOf("message" to "Failed to start session: ${e.message}"), outputJson)
|
||||
null
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
private suspend fun handleMessage(
|
||||
session: WebSocketSession,
|
||||
msgJson: JsonObject?,
|
||||
ctx: RunContext,
|
||||
setExitCode: (Int) -> Unit,
|
||||
): Boolean {
|
||||
val type = msgJson?.get("type")?.jsonPrimitive?.content ?: "unknown"
|
||||
fun field(key: String): String? = msgJson?.get(key)?.jsonPrimitive?.content
|
||||
|
||||
return when (type) {
|
||||
"SessionCompleted" -> {
|
||||
printLine("session_completed", mapOf("sessionId" to ctx.sid), ctx.outputJson)
|
||||
setExitCode(0)
|
||||
false
|
||||
}
|
||||
"SessionFailed" -> {
|
||||
val reason = field("reason") ?: "unknown"
|
||||
printLine("session_failed", mapOf("sessionId" to ctx.sid, "reason" to reason), ctx.outputJson)
|
||||
setExitCode(1)
|
||||
false
|
||||
}
|
||||
"StageStarted" -> {
|
||||
printLine("stage_started", mapOf("stageId" to (field("stageId") ?: "")), ctx.outputJson)
|
||||
true
|
||||
}
|
||||
"StageCompleted" -> {
|
||||
printLine("stage_completed", mapOf("stageId" to (field("stageId") ?: "")), ctx.outputJson)
|
||||
true
|
||||
}
|
||||
"StageFailed" -> {
|
||||
printLine(
|
||||
"stage_failed",
|
||||
mapOf("stageId" to (field("stageId") ?: ""), "reason" to (field("reason") ?: "")),
|
||||
ctx.outputJson,
|
||||
)
|
||||
true
|
||||
}
|
||||
"ToolStarted" -> {
|
||||
printLine("tool_started", mapOf("tool" to (field("toolName") ?: "")), ctx.outputJson)
|
||||
true
|
||||
}
|
||||
"ToolCompleted" -> {
|
||||
printLine("tool_completed", mapOf("tool" to (field("toolName") ?: "")), ctx.outputJson)
|
||||
true
|
||||
}
|
||||
"ToolFailed" -> {
|
||||
printLine(
|
||||
"tool_failed",
|
||||
mapOf("tool" to (field("toolName") ?: ""), "reason" to (field("reason") ?: "")),
|
||||
ctx.outputJson,
|
||||
)
|
||||
true
|
||||
}
|
||||
"ToolRejected" -> {
|
||||
printLine(
|
||||
"tool_rejected",
|
||||
mapOf("tool" to (field("toolName") ?: ""), "reason" to (field("reason") ?: "")),
|
||||
ctx.outputJson,
|
||||
)
|
||||
true
|
||||
}
|
||||
"ApprovalRequired" -> {
|
||||
val approvalCtx = ApprovalContext(
|
||||
session = session,
|
||||
requestId = field("requestId") ?: "",
|
||||
toolName = field("toolName") ?: "",
|
||||
preview = field("preview"),
|
||||
)
|
||||
handleApproval(approvalCtx, ctx, setExitCode)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleApproval(
|
||||
approvalCtx: ApprovalContext,
|
||||
ctx: RunContext,
|
||||
setExitCode: (Int) -> Unit,
|
||||
): Boolean {
|
||||
val decision = resolveDecision(ctx, approvalCtx.toolName, approvalCtx.preview)
|
||||
|
||||
val steeringNote = if (decision == "STEER" && ctx.isTty) {
|
||||
print("Steering note: ")
|
||||
readLine()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val response = ApprovalResponsePayload(
|
||||
type = "ApprovalResponse",
|
||||
requestId = approvalCtx.requestId,
|
||||
decision = decision,
|
||||
steeringNote = steeringNote,
|
||||
)
|
||||
approvalCtx.session.send(serializer.encodeToString(response))
|
||||
|
||||
return if (decision == "REJECT" && !ctx.autoApprove && !ctx.isTty) {
|
||||
setExitCode(2)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDecision(ctx: RunContext, toolName: String, preview: String?): String = when {
|
||||
ctx.autoApprove -> "APPROVE"
|
||||
!ctx.isTty -> {
|
||||
System.err.println(
|
||||
"WARNING: approval required but no TTY and --auto-approve not set — denying"
|
||||
)
|
||||
"REJECT"
|
||||
}
|
||||
else -> promptApproval(toolName, preview)
|
||||
}
|
||||
|
||||
private fun promptApproval(toolName: String, preview: String?): String {
|
||||
println("\nApproval required for tool: $toolName")
|
||||
preview?.let { println("Preview: $it") }
|
||||
print("[a]pprove / [r]eject / [s]teer: ")
|
||||
return when (readLine()?.trim()?.lowercase()) {
|
||||
"a", "approve" -> "APPROVE"
|
||||
"s", "steer" -> "STEER"
|
||||
else -> "REJECT"
|
||||
}
|
||||
}
|
||||
|
||||
private fun printLine(event: String, data: Map<String, String?>, outputJson: Boolean) {
|
||||
if (outputJson) {
|
||||
val fields = data.entries.joinToString(", ") { (k, v) ->
|
||||
"\"$k\": ${if (v == null) "null" else "\"$v\""}"
|
||||
}
|
||||
println("{\"event\": \"$event\", $fields}")
|
||||
} else {
|
||||
val details = data.entries.joinToString(" ") { (k, v) -> "$k=${v ?: "null"}" }
|
||||
println("[$event] $details")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SystemExitException(val code: Int) : Exception("exit $code")
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class SessionCommand : CliktCommand(name = "session") {
|
||||
override fun run() = Unit
|
||||
|
||||
init {
|
||||
subcommands(
|
||||
SessionListCommand(),
|
||||
SessionResumeCommand(),
|
||||
SessionCancelCommand(),
|
||||
SessionEventsCommand(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parentJsonFlag(command: CliktCommand): Boolean =
|
||||
(command.currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
|
||||
private val sessionJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
class SessionListCommand : CliktCommand(name = "list") {
|
||||
private val limit by option("--limit", help = "Max sessions to return").default("20")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = parentJsonFlag(this@SessionListCommand)
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(sessionJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val resp = client.get("http://$host:$portInt/sessions?limit=$limit")
|
||||
val body = resp.body<JsonArray>()
|
||||
if (outputJson) {
|
||||
println(body.toString())
|
||||
} else {
|
||||
body.forEach { el ->
|
||||
val obj = el as? JsonObject ?: return@forEach
|
||||
val sid = obj["sessionId"]?.jsonPrimitive?.content ?: ""
|
||||
val wf = obj["workflowId"]?.jsonPrimitive?.content ?: ""
|
||||
val status = obj["status"]?.jsonPrimitive?.content ?: ""
|
||||
println("$sid workflow=$wf status=$status")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error listing sessions: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
class SessionResumeCommand : CliktCommand(name = "resume") {
|
||||
private val id by argument("SESSION_ID")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = parentJsonFlag(this@SessionResumeCommand)
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(sessionJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
client.post("http://$host:$portInt/sessions/$id/resume")
|
||||
if (outputJson) {
|
||||
println("{\"sessionId\": \"$id\", \"action\": \"resumed\"}")
|
||||
} else {
|
||||
println("Session $id resumed.")
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error resuming session: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
class SessionCancelCommand : CliktCommand(name = "cancel") {
|
||||
private val id by argument("SESSION_ID")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = parentJsonFlag(this@SessionCancelCommand)
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(sessionJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
client.post("http://$host:$portInt/sessions/$id/cancel")
|
||||
if (outputJson) {
|
||||
println("{\"sessionId\": \"$id\", \"action\": \"cancelled\"}")
|
||||
} else {
|
||||
println("Session $id cancelled.")
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error cancelling session: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
class SessionEventsCommand : CliktCommand(name = "events") {
|
||||
private val id by argument("SESSION_ID")
|
||||
private val from by option("--from", help = "Start from event ID")
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = parentJsonFlag(this@SessionEventsCommand)
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(sessionJson) }
|
||||
}
|
||||
|
||||
val query = from?.let { "?from=$it" } ?: ""
|
||||
|
||||
runCatching {
|
||||
val resp = client.get("http://$host:$portInt/sessions/$id/events$query")
|
||||
val body = resp.body<JsonArray>()
|
||||
if (outputJson) {
|
||||
println(body.toString())
|
||||
} else {
|
||||
body.forEachIndexed { idx, el ->
|
||||
println("[$idx] $el")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error fetching events: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.correx.apps.cli.commands
|
||||
|
||||
import com.correx.apps.cli.CorrexCli
|
||||
import com.correx.apps.cli.DEFAULT_PORT
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class StatusCommand : CliktCommand(name = "status") {
|
||||
private val host by option("--host").default("localhost")
|
||||
private val port by option("--port").default("$DEFAULT_PORT")
|
||||
|
||||
private val statusJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override fun run(): Unit = runBlocking {
|
||||
val portInt = port.toIntOrNull() ?: DEFAULT_PORT
|
||||
val outputJson = (currentContext.findRoot().command as? CorrexCli)?.json ?: false
|
||||
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) { json(statusJson) }
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val resp = client.get("http://$host:$portInt/status")
|
||||
val body = resp.body<JsonObject>()
|
||||
if (outputJson) {
|
||||
println(body.toString())
|
||||
} else {
|
||||
body.entries.forEach { (k, v) ->
|
||||
println("$k: ${v.jsonPrimitive.content}")
|
||||
}
|
||||
}
|
||||
}.getOrElse { e ->
|
||||
System.err.println("Error fetching status: ${e.message}")
|
||||
}
|
||||
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.correx.apps.cli.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.readText
|
||||
|
||||
class CliWsClient(private val httpClient: HttpClient) {
|
||||
suspend fun sessionStream(
|
||||
host: String,
|
||||
port: Int,
|
||||
sessionId: String,
|
||||
onMessage: suspend (WebSocketSession, String) -> Boolean,
|
||||
) {
|
||||
httpClient.webSocket(host = host, port = port, path = "/sessions/$sessionId/stream") {
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val keepGoing = onMessage(this, frame.readText())
|
||||
if (!keepGoing) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user