refactor: rename cli and tui tools
This commit is contained in:
20
README.md
20
README.md
@@ -91,28 +91,28 @@ You can add/remove `*.hub.kts` files in `scripts/` at any time. The web host res
|
|||||||
- For script files with custom extension (`*.hub.kts`), IDEA code insight is usually weaker than standard `*.main.kts` or module Kotlin sources. This is an IDE limitation for custom script definitions.
|
- For script files with custom extension (`*.hub.kts`), IDEA code insight is usually weaker than standard `*.main.kts` or module Kotlin sources. This is an IDE limitation for custom script definitions.
|
||||||
|
|
||||||
## Command CLI
|
## Command CLI
|
||||||
A standalone CLI script is available at `tools/api-cli.main.kts` (independent from host internals, only HTTP calls).
|
A standalone CLI script is available at `tools/slhaf-hub-cli.kts` (independent from host internals, only HTTP calls).
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```bash
|
```bash
|
||||||
kotlin tools/api-cli.main.kts --base-url=http://127.0.0.1:8080 --token-file=./scripts/.host-api-token list
|
kotlin tools/slhaf-hub-cli.kts --base-url=http://127.0.0.1:8080 --token-file=./scripts/.host-api-token list
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token type
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token type
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token show hello
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token show hello
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token run hello --arg=name=Alice --arg=upper=true
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token run hello --arg=name=Alice --arg=upper=true
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token create demo --text='// @desc: demo\nval args: Array<String> = emptyArray()\nprintln("ok")'
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token create demo --text='// @desc: demo\nval args: Array<String> = emptyArray()\nprintln("ok")'
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token sub-create demo-sub --scripts=hello,time
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token sub-create demo-sub --scripts=hello,time
|
||||||
kotlin tools/api-cli.main.kts --token-file=./scripts/.host-api-token sub-list
|
kotlin tools/slhaf-hub-cli.kts --token-file=./scripts/.host-api-token sub-list
|
||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
- In this environment, `elide run <kts> -- <args...>` currently does not expose Kotlin script args reliably; use `kotlin` to run the CLI script.
|
- In this environment, `elide run <kts> -- <args...>` currently does not expose Kotlin script args reliably; use `kotlin` to run the CLI script.
|
||||||
|
|
||||||
## Simple TUI
|
## Simple TUI
|
||||||
A minimal keyboard-driven TUI is available at `tools/api-tui.main.kts`.
|
A minimal keyboard-driven TUI is available at `tools/slhaf-hub-tui.kts`.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
```bash
|
```bash
|
||||||
kotlin tools/api-tui.main.kts --base-url=http://127.0.0.1:8080 --token-file=./scripts/.host-api-token
|
kotlin tools/slhaf-hub-tui.kts --base-url=http://127.0.0.1:8080 --token-file=./scripts/.host-api-token
|
||||||
```
|
```
|
||||||
|
|
||||||
Keys:
|
Keys:
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
#!/usr/bin/env kotlin
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URLEncoder
|
|
||||||
import java.net.http.HttpClient
|
|
||||||
import java.net.http.HttpRequest
|
|
||||||
import java.net.http.HttpResponse
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
val ENV_API_BASE_URL = "HOST_API_BASE_URL"
|
|
||||||
val ENV_API_TOKEN = "HOST_API_TOKEN"
|
|
||||||
|
|
||||||
data class GlobalOptions(
|
|
||||||
val baseUrl: String,
|
|
||||||
val token: String?,
|
|
||||||
val tokenFile: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ParsedInput(
|
|
||||||
val global: GlobalOptions,
|
|
||||||
val command: String,
|
|
||||||
val commandArgs: List<String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun usage(): String =
|
|
||||||
"""
|
|
||||||
Usage:
|
|
||||||
elide run api-cli.main.kts [global options] <command> [command options]
|
|
||||||
|
|
||||||
Global options:
|
|
||||||
--base-url=<url> Default: HOST_API_BASE_URL or http://127.0.0.1:8080
|
|
||||||
--token=<token> Authorization token
|
|
||||||
--token-file=<path> Load token from file (fallback: HOST_API_TOKEN env)
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
health
|
|
||||||
type
|
|
||||||
list
|
|
||||||
show <script>
|
|
||||||
meta <script>
|
|
||||||
run <script> [--arg=k=v ...] [--body=text] [--post]
|
|
||||||
create <script> (--file=<path> | --text=<content>)
|
|
||||||
update <script> (--file=<path> | --text=<content>)
|
|
||||||
delete <script>
|
|
||||||
|
|
||||||
sub-list
|
|
||||||
sub-show <name>
|
|
||||||
sub-create <name> --scripts=a,b,c
|
|
||||||
sub-update <name> --scripts=a,b,c
|
|
||||||
sub-delete <name>
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token type
|
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token sub-list
|
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token sub-create demo --scripts=hello,time
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
fun parseInput(args: List<String>): ParsedInput {
|
|
||||||
if (args.isEmpty() || args.contains("--help") || args.contains("-h")) {
|
|
||||||
println(usage())
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseUrl = System.getenv(ENV_API_BASE_URL)?.trim().orEmpty().ifBlank { "http://127.0.0.1:8080" }
|
|
||||||
var token: String? = null
|
|
||||||
var tokenFile: String? = null
|
|
||||||
var i = 0
|
|
||||||
while (i < args.size && args[i].startsWith("--")) {
|
|
||||||
val arg = args[i]
|
|
||||||
when {
|
|
||||||
arg.startsWith("--base-url=") -> baseUrl = arg.substringAfter("=")
|
|
||||||
arg.startsWith("--token=") -> token = arg.substringAfter("=")
|
|
||||||
arg.startsWith("--token-file=") -> tokenFile = arg.substringAfter("=")
|
|
||||||
else -> break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i >= args.size) error("Missing command.\n${usage()}")
|
|
||||||
val command = args[i]
|
|
||||||
val commandArgs = args.drop(i + 1)
|
|
||||||
return ParsedInput(GlobalOptions(baseUrl.trimEnd('/'), token, tokenFile), command, commandArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readToken(options: GlobalOptions): String? {
|
|
||||||
if (!options.token.isNullOrBlank()) return options.token
|
|
||||||
if (!options.tokenFile.isNullOrBlank()) {
|
|
||||||
val file = File(options.tokenFile)
|
|
||||||
if (!file.exists()) error("Token file not found: ${file.absolutePath}")
|
|
||||||
return file.readText().trim().ifBlank { null }
|
|
||||||
}
|
|
||||||
return System.getenv(ENV_API_TOKEN)?.trim()?.ifBlank { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun encode(value: String): String = URLEncoder.encode(value, StandardCharsets.UTF_8)
|
|
||||||
|
|
||||||
fun requireScriptName(args: List<String>): String {
|
|
||||||
if (args.isEmpty()) error("Missing <script> argument.")
|
|
||||||
return args.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requireNameArg(args: List<String>, label: String): String {
|
|
||||||
if (args.isEmpty()) error("Missing <$label> argument.")
|
|
||||||
return args.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBodyAndSource(args: List<String>): Pair<String?, String?> {
|
|
||||||
val fileArg = args.firstOrNull { it.startsWith("--file=") }?.substringAfter("=")
|
|
||||||
val textArg = args.firstOrNull { it.startsWith("--text=") }?.substringAfter("=")
|
|
||||||
if (fileArg != null && textArg != null) error("Use either --file or --text, not both.")
|
|
||||||
if (fileArg == null && textArg == null) error("Missing --file or --text.")
|
|
||||||
return fileArg to textArg
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseRunArgs(args: List<String>): Triple<String, List<Pair<String, String>>, Pair<Boolean, String?>> {
|
|
||||||
val script = requireScriptName(args)
|
|
||||||
val rest = args.drop(1)
|
|
||||||
val query = mutableListOf<Pair<String, String>>()
|
|
||||||
var body: String? = null
|
|
||||||
var post = false
|
|
||||||
for (arg in rest) {
|
|
||||||
when {
|
|
||||||
arg == "--post" -> {
|
|
||||||
post = true
|
|
||||||
}
|
|
||||||
|
|
||||||
arg.startsWith("--body=") -> {
|
|
||||||
body = arg.substringAfter("=")
|
|
||||||
}
|
|
||||||
|
|
||||||
arg.startsWith("--arg=") -> {
|
|
||||||
val token = arg.substringAfter("--arg=")
|
|
||||||
val idx = token.indexOf('=')
|
|
||||||
if (idx <= 0) error("Invalid --arg format: $arg, expected --arg=key=value")
|
|
||||||
query += token.substring(0, idx) to token.substring(idx + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
error("Unknown run option: $arg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Triple(script, query, post to body)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseScriptsArg(args: List<String>): Set<String> {
|
|
||||||
val raw = args.firstOrNull { it.startsWith("--scripts=") }?.substringAfter("=")
|
|
||||||
?: error("Missing --scripts=a,b,c")
|
|
||||||
val items =
|
|
||||||
raw.split(',')
|
|
||||||
.map { it.trim() }
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.toSet()
|
|
||||||
if (items.any { !Regex("[A-Za-z0-9._-]+$").matches(it) }) {
|
|
||||||
error("Invalid script names in --scripts, only [A-Za-z0-9._-] allowed")
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
fun request(
|
|
||||||
client: HttpClient,
|
|
||||||
baseUrl: String,
|
|
||||||
token: String?,
|
|
||||||
method: String,
|
|
||||||
path: String,
|
|
||||||
body: String? = null,
|
|
||||||
): Pair<Int, String> {
|
|
||||||
val reqBuilder =
|
|
||||||
HttpRequest
|
|
||||||
.newBuilder(URI.create("$baseUrl$path"))
|
|
||||||
.header("Accept", "text/plain,application/json")
|
|
||||||
if (!token.isNullOrBlank()) reqBuilder.header("Authorization", "Bearer $token")
|
|
||||||
|
|
||||||
val request =
|
|
||||||
when (method) {
|
|
||||||
"GET" -> reqBuilder.GET().build()
|
|
||||||
"DELETE" -> reqBuilder.DELETE().build()
|
|
||||||
"POST" -> reqBuilder.header("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body ?: "")).build()
|
|
||||||
"PUT" -> reqBuilder.header("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
.PUT(HttpRequest.BodyPublishers.ofString(body ?: "")).build()
|
|
||||||
else -> error("Unsupported method: $method")
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
|
||||||
return response.statusCode() to response.body()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
try {
|
|
||||||
val input = parseInput(args.toList())
|
|
||||||
val token = readToken(input.global)
|
|
||||||
val client = HttpClient.newHttpClient()
|
|
||||||
val base = input.global.baseUrl
|
|
||||||
|
|
||||||
val (status, body) =
|
|
||||||
when (input.command) {
|
|
||||||
"health" -> request(client, base, null, "GET", "/health")
|
|
||||||
"type" -> request(client, base, token, "GET", "/type")
|
|
||||||
"list" -> request(client, base, token, "GET", "/scripts")
|
|
||||||
"show" -> {
|
|
||||||
val script = requireScriptName(input.commandArgs)
|
|
||||||
request(client, base, token, "GET", "/scripts/${encode(script)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"meta" -> {
|
|
||||||
val script = requireScriptName(input.commandArgs)
|
|
||||||
request(client, base, token, "GET", "/meta/${encode(script)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"run" -> {
|
|
||||||
val (script, queryPairs, postAndBody) = parseRunArgs(input.commandArgs)
|
|
||||||
val query =
|
|
||||||
if (queryPairs.isEmpty()) "" else queryPairs.joinToString("&", prefix = "?") { (k, v) -> "${encode(k)}=${encode(v)}" }
|
|
||||||
val (post, postBody) = postAndBody
|
|
||||||
val method = if (post) "POST" else "GET"
|
|
||||||
request(client, base, token, method, "/run/${encode(script)}$query", postBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
"create" -> {
|
|
||||||
val script = requireScriptName(input.commandArgs)
|
|
||||||
val (fileArg, textArg) = parseBodyAndSource(input.commandArgs.drop(1))
|
|
||||||
val bodyContent = if (fileArg != null) File(fileArg).readText() else textArg ?: ""
|
|
||||||
request(client, base, token, "POST", "/scripts/${encode(script)}", bodyContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
"update" -> {
|
|
||||||
val script = requireScriptName(input.commandArgs)
|
|
||||||
val (fileArg, textArg) = parseBodyAndSource(input.commandArgs.drop(1))
|
|
||||||
val bodyContent = if (fileArg != null) File(fileArg).readText() else textArg ?: ""
|
|
||||||
request(client, base, token, "PUT", "/scripts/${encode(script)}", bodyContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
"delete" -> {
|
|
||||||
val script = requireScriptName(input.commandArgs)
|
|
||||||
request(client, base, token, "DELETE", "/scripts/${encode(script)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"sub-list" -> request(client, base, token, "GET", "/subtokens")
|
|
||||||
"sub-show" -> {
|
|
||||||
val name = requireNameArg(input.commandArgs, "name")
|
|
||||||
request(client, base, token, "GET", "/subtokens/${encode(name)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
"sub-create" -> {
|
|
||||||
val name = requireNameArg(input.commandArgs, "name")
|
|
||||||
val scripts = parseScriptsArg(input.commandArgs.drop(1))
|
|
||||||
request(client, base, token, "POST", "/subtokens/${encode(name)}", scripts.joinToString("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
"sub-update" -> {
|
|
||||||
val name = requireNameArg(input.commandArgs, "name")
|
|
||||||
val scripts = parseScriptsArg(input.commandArgs.drop(1))
|
|
||||||
request(client, base, token, "PUT", "/subtokens/${encode(name)}", scripts.joinToString("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
"sub-delete" -> {
|
|
||||||
val name = requireNameArg(input.commandArgs, "name")
|
|
||||||
request(client, base, token, "DELETE", "/subtokens/${encode(name)}")
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> error("Unknown command: ${input.command}\n${usage()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
println(body)
|
|
||||||
if (status >= 400) {
|
|
||||||
System.err.println("[HTTP $status]")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
System.err.println("Error: ${e.message}")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main(args)
|
|
||||||
@@ -27,7 +27,7 @@ data class ParsedInput(
|
|||||||
fun usage(): String =
|
fun usage(): String =
|
||||||
"""
|
"""
|
||||||
Usage:
|
Usage:
|
||||||
elide run api-cli.main.kts [global options] <command> [command options]
|
kotlin slhaf-hub-cli.kts [global options] <command> [command options]
|
||||||
|
|
||||||
Global options:
|
Global options:
|
||||||
--base-url=<url> Default: HOST_API_BASE_URL or http://127.0.0.1:8080
|
--base-url=<url> Default: HOST_API_BASE_URL or http://127.0.0.1:8080
|
||||||
@@ -52,9 +52,9 @@ Commands:
|
|||||||
sub-delete <name>
|
sub-delete <name>
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token type
|
kotlin slhaf-hub-cli.kts --token-file=./scripts/.host-api-token type
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token sub-list
|
kotlin slhaf-hub-cli.kts --token-file=./scripts/.host-api-token sub-list
|
||||||
elide run api-cli.main.kts --token-file=./scripts/.host-api-token sub-create demo --scripts=hello,time
|
kotlin slhaf-hub-cli.kts --token-file=./scripts/.host-api-token sub-create demo --scripts=hello,time
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun parseInput(args: List<String>): ParsedInput {
|
fun parseInput(args: List<String>): ParsedInput {
|
||||||
@@ -66,7 +66,7 @@ private fun selected(text: String) = "$BG_BLUE$FG_BLACK$BOLD$text$RESET"
|
|||||||
fun usage(): String =
|
fun usage(): String =
|
||||||
"""
|
"""
|
||||||
Usage:
|
Usage:
|
||||||
kotlin tools/api-tui.main.kts [--base-url=http://127.0.0.1:8080] [--token=<token> | --token-file=./scripts/.host-api-token]
|
kotlin tools/slhaf-hub-tui.kts [--base-url=http://127.0.0.1:8080] [--token=<token> | --token-file=./scripts/.host-api-token]
|
||||||
|
|
||||||
Layout:
|
Layout:
|
||||||
Actions:
|
Actions:
|
||||||
Reference in New Issue
Block a user