Compare commits

..

9 Commits

Author SHA1 Message Date
6f1134a6fb Merge pull request '1.4.1 release' (#10) from develop into main
All checks were successful
/ publish (push) Successful in 3m54s
Reviewed-on: Infendro/cli-kt#10
2026-01-29 16:48:33 +00:00
21769c20c4 enforce exception message 2026-01-18 21:45:55 +01:00
d5a66f6853 bump version to 1.4.1 2026-01-17 16:02:14 +01:00
f55869193e add additional targets 2026-01-17 16:00:36 +01:00
9ad2d2c5e0 rename ci 2026-01-17 15:59:07 +01:00
a5b578a47d split error and exception 2026-01-17 15:57:05 +01:00
dbd9d53050 improve ci 2026-01-13 15:39:50 +01:00
87e6e65809 improve string distance documentation 2026-01-10 15:12:36 +01:00
54972c36b6 refactor package structure 2026-01-10 09:32:55 +01:00
40 changed files with 137 additions and 120 deletions

View File

@@ -1,36 +0,0 @@
on:
push:
branches:
- main
env:
INFENDRO__TOKEN: ${{ secrets.TOKEN }}
jobs:
publish:
runs-on: linux
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v4
with:
key: gradle
path: |
~/.gradle/caches/
~/.gradle/wrapper/
~/.konan/
- uses: actions/setup-java@v4
with:
java-version: '24'
distribution: 'temurin'
- run: ./gradlew publish
- uses: actions/cache/save@v4
with:
key: gradle
path: |
~/.gradle/caches/
~/.gradle/wrapper/
~/.konan/

View File

@@ -0,0 +1,47 @@
on:
push:
branches: [ main ]
jobs:
publish:
runs-on: linux
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: '24'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v4
with:
key: gradle-${{ hashFiles('**/libs.versions.toml') }}
restore-keys: gradle
path: |
~/.gradle/caches/
~/.gradle/wrapper/
- name: Cache Konan
uses: actions/cache@v4
with:
key: konan-${{ hashFiles('**/libs.versions.toml') }}
restore-keys: konan
path: |
~/.konan/
- name: Cache Node
uses: actions/cache@v4
with:
key: node-${{ hashFiles('**/libs.versions.toml') }}
restore-keys: node
path: |
~/.gradle/nodejs
~/.gradle/yarn
- name: Publish
run: ./gradlew publish
env:
INFENDRO__TOKEN: ${{ secrets.TOKEN }}

View File

@@ -2,15 +2,6 @@
`cli-kt` is a Kotlin Multiplatform framework for implementing terminal applications. `cli-kt` is a Kotlin Multiplatform framework for implementing terminal applications.
## Targets
| Target | Status |
|------------------|---------------|
| JVM | Supported |
| JavaScript | Supported |
| Native (Linux) | Supported |
| Native (Windows) | Not Supported |
## Installation ## Installation
Add the following to your `build.gradle.kts`. Add the following to your `build.gradle.kts`.
@@ -21,7 +12,7 @@ repositories {
} }
dependencies { dependencies {
implementation("com.infendro:cli:1.4.0") implementation("com.infendro:cli:1.4.1")
} }
``` ```

View File

@@ -1,8 +1,11 @@
@file:OptIn(ExperimentalWasmDsl::class)
import com.infendro.plugin.infendro import com.infendro.plugin.infendro
import com.infendro.plugin.token import com.infendro.plugin.token
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
group = "com.infendro" group = "com.infendro"
version = "1.4.0" version = "1.4.1"
plugins { plugins {
alias(libs.plugins.infendro) alias(libs.plugins.infendro)
@@ -12,10 +15,14 @@ plugins {
kotlin { kotlin {
jvm() jvm()
js {
nodejs()
}
linuxX64() linuxX64()
linuxArm64()
mingwX64()
macosX64()
macosArm64()
js { nodejs() }
wasmJs { nodejs() }
wasmWasi { nodejs() }
repositories { repositories {
mavenCentral() mavenCentral()

View File

@@ -1,6 +1,6 @@
[versions] [versions]
infendro = "1.0.0" infendro = "1.0.0"
kotlin = "2.2.21" kotlin = "2.3.0"
[plugins] [plugins]
infendro = { id = "com.infendro.plugin", version.ref = "infendro" } infendro = { id = "com.infendro.plugin", version.ref = "infendro" }

View File

@@ -1,7 +1,7 @@
package com.infendro.cli.command.argument package com.infendro.cli.argument
import com.infendro.cli.error.build.InvalidArgumentName import com.infendro.cli.exception.InvalidArgumentName
import com.infendro.cli.error.build.InvalidRange import com.infendro.cli.exception.InvalidRange
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex.ARGUMENT import com.infendro.cli.util.Regex.ARGUMENT

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.command.argument package com.infendro.cli.argument
import com.infendro.cli.parser.* import com.infendro.cli.parser.*

View File

@@ -1,13 +1,13 @@
package com.infendro.cli.command package com.infendro.cli.command
import com.infendro.cli.Dsl import com.infendro.cli.Dsl
import com.infendro.cli.command.argument.Argument import com.infendro.cli.argument.Argument
import com.infendro.cli.command.context.Context import com.infendro.cli.command.context.Context
import com.infendro.cli.command.context.ContextParser import com.infendro.cli.command.context.ContextParser
import com.infendro.cli.command.help.DefaultHelpRenderer import com.infendro.cli.command.help.DefaultHelpRenderer
import com.infendro.cli.command.help.HelpRenderer import com.infendro.cli.command.help.HelpRenderer
import com.infendro.cli.command.option.Option import com.infendro.cli.exception.*
import com.infendro.cli.error.build.* import com.infendro.cli.option.Option
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex import com.infendro.cli.util.Regex
@@ -33,7 +33,7 @@ sealed class Command private constructor(
if (help != null) print(help) if (help != null) print(help)
} }
is ContextParser.Result.Failure -> { is ContextParser.Result.Failure -> {
println("error: ${result.failure.message}") println("error: ${result.error.message}")
val command = result.path.last() val command = result.path.last()
val help = command.renderer.render(result.path) val help = command.renderer.render(result.path)
if (help != null) print("\n$help") if (help != null) print("\n$help")

View File

@@ -2,8 +2,8 @@ package com.infendro.cli.command.context
import com.infendro.cli.Dsl import com.infendro.cli.Dsl
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.command.argument.Argument import com.infendro.cli.argument.Argument
import com.infendro.cli.command.option.Option import com.infendro.cli.option.Option
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@Dsl @Dsl

View File

@@ -1,7 +1,7 @@
package com.infendro.cli.command.context package com.infendro.cli.command.context
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.error.run.* import com.infendro.cli.error.*
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
internal class ContextParser( internal class ContextParser(
@@ -19,13 +19,13 @@ internal class ContextParser(
data class Failure( data class Failure(
val path: List<Command>, val path: List<Command>,
val failure: RuntimeFailure, val error: Error,
) : Result() ) : Result()
} }
private fun success(context: Context) = Result.Success(context) private fun success(context: Context) = Result.Success(context)
private fun help(path: List<Command>) = Result.Help(path) private fun help(path: List<Command>) = Result.Help(path)
private fun failure(path: List<Command>, exception: RuntimeFailure) = Result.Failure(path, exception) private fun failure(path: List<Command>, error: Error) = Result.Failure(path, error)
private var index = 0 private var index = 0
private val current: String private val current: String
@@ -63,7 +63,7 @@ internal class ContextParser(
if (cmd is Command.Fallback<*>) { if (cmd is Command.Fallback<*>) {
val value = when (val result = cmd.parser.parse(current)) { val value = when (val result = cmd.parser.parse(current)) {
is Parser.Result.Success<*> -> result.value is Parser.Result.Success<*> -> result.value
is Parser.Result.Failure<*> -> return failure(path, result.failure) is Parser.Result.Failure<*> -> return failure(path, result.error)
} }
commands[cmd.key] = value commands[cmd.key] = value
} }
@@ -91,7 +91,7 @@ internal class ContextParser(
val value = when (val result = argument.parser.parse(current)) { val value = when (val result = argument.parser.parse(current)) {
is Parser.Result.Success<*> -> result.value is Parser.Result.Success<*> -> result.value
is Parser.Result.Failure<*> -> return failure(path, result.failure) is Parser.Result.Failure<*> -> return failure(path, result.error)
} }
arguments[argument]!! += value arguments[argument]!! += value
argumentCount++ argumentCount++
@@ -141,7 +141,7 @@ internal class ContextParser(
text != null -> text != null ->
when (val result = option.parser.parse(text)) { when (val result = option.parser.parse(text)) {
is Parser.Result.Success<*> -> result.value is Parser.Result.Success<*> -> result.value
is Parser.Result.Failure<*> -> return failure(path, result.failure) is Parser.Result.Failure<*> -> return failure(path, result.error)
} }
option.flag -> option.fallback!! option.flag -> option.fallback!!
else -> return failure(path, MissingOptionValue(option)) else -> return failure(path, MissingOptionValue(option))

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.command.help package com.infendro.cli.command.help
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.command.argument.Argument import com.infendro.cli.argument.Argument
import com.infendro.cli.command.option.Option import com.infendro.cli.option.Option
import kotlin.reflect.KClass import kotlin.reflect.KClass
object DefaultHelpRenderer : HelpRenderer { object DefaultHelpRenderer : HelpRenderer {

View File

@@ -0,0 +1,5 @@
package com.infendro.cli.error
abstract class Error {
abstract val message: String
}

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import com.infendro.cli.command.argument.Argument import com.infendro.cli.argument.Argument
class InvalidArgumentCount( class InvalidArgumentCount(
val argument: Argument<*>, val argument: Argument<*>,
val count: Int, val count: Int,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() { get() {
val expected = when { val expected = when {

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import com.infendro.cli.command.option.Option import com.infendro.cli.option.Option
class InvalidOptionCount( class InvalidOptionCount(
val option: Option<*>, val option: Option<*>,
val count: Int, val count: Int,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() { get() {
val expected = when { val expected = when {

View File

@@ -1,11 +1,11 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import kotlin.reflect.KClass import kotlin.reflect.KClass
class InvalidValue( class InvalidValue(
val value: String, val value: String,
val type: KClass<*>, val type: KClass<*>,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}.""" get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}."""
} }

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.error.run package com.infendro.cli.error
class MalformedOption( class MalformedOption(
val option: String, val option: String,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = """Malformed option "$option".""" get() = """Malformed option "$option"."""
} }

View File

@@ -1,10 +1,10 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import com.infendro.cli.command.option.Option import com.infendro.cli.option.Option
class MissingOptionValue( class MissingOptionValue(
val option: Option<*>, val option: Option<*>,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = """No value provided for option "${option.name}".""" get() = """No value provided for option "${option.name}"."""
} }

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.error.run package com.infendro.cli.error
class UnexpectedArgument( class UnexpectedArgument(
val argument: String, val argument: String,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = """Unexpected argument "$argument".""" get() = """Unexpected argument "$argument"."""
} }

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.util.distance import com.infendro.cli.util.distance
@@ -8,7 +8,7 @@ import kotlin.math.min
class UnknownCommand( class UnknownCommand(
val command: Command, val command: Command,
val name: String, val name: String,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = buildString { get() = buildString {
val suggestion = command.commands val suggestion = command.commands

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.run package com.infendro.cli.error
import com.infendro.cli.command.Command import com.infendro.cli.command.Command
import com.infendro.cli.util.distance import com.infendro.cli.util.distance
@@ -8,7 +8,7 @@ import kotlin.math.min
class UnknownOption( class UnknownOption(
val command: Command, val command: Command,
val name: String, val name: String,
) : RuntimeFailure() { ) : Error() {
override val message: String override val message: String
get() = buildString { get() = buildString {
val suggestion = command.options val suggestion = command.options

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.error.build
abstract class BuildException : Exception()

View File

@@ -1,5 +0,0 @@
package com.infendro.cli.error.run
abstract class RuntimeFailure {
abstract val message: String
}

View File

@@ -0,0 +1,5 @@
package com.infendro.cli.exception
abstract class BuildException : Exception() {
abstract override val message: String
}

View File

@@ -1,6 +1,6 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
import com.infendro.cli.command.argument.Argument import com.infendro.cli.argument.Argument
class DuplicateArgument( class DuplicateArgument(
val argument: Argument<*>, val argument: Argument<*>,

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
import com.infendro.cli.command.Command import com.infendro.cli.command.Command

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class DuplicateExecute : BuildException() { class DuplicateExecute : BuildException() {
override val message: String override val message: String

View File

@@ -1,6 +1,6 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
import com.infendro.cli.command.option.Option import com.infendro.cli.option.Option
class DuplicateOption( class DuplicateOption(
val option: Option<*>, val option: Option<*>,

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class DuplicateRenderer : BuildException() { class DuplicateRenderer : BuildException() {
override val message: String override val message: String

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidArgumentName( class InvalidArgumentName(
val name: String, val name: String,

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidArgumentOrder : BuildException() { class InvalidArgumentOrder : BuildException() {
override val message: String override val message: String

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidCommandName( class InvalidCommandName(
val name: String, val name: String,

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidCommandOrder : BuildException() { class InvalidCommandOrder : BuildException() {
override val message: String override val message: String

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidFallback : BuildException() { class InvalidFallback : BuildException() {
override val message: String override val message: String

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidOptionName( class InvalidOptionName(
val name: String, val name: String,

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class InvalidRange : BuildException() { class InvalidRange : BuildException() {
override val message: String override val message: String

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.error.build package com.infendro.cli.exception
class MissingOptionName : BuildException() { class MissingOptionName : BuildException() {
override val message: String override val message: String

View File

@@ -1,8 +1,8 @@
package com.infendro.cli.command.option package com.infendro.cli.option
import com.infendro.cli.error.build.InvalidOptionName import com.infendro.cli.exception.InvalidOptionName
import com.infendro.cli.error.build.InvalidRange import com.infendro.cli.exception.InvalidRange
import com.infendro.cli.error.build.MissingOptionName import com.infendro.cli.exception.MissingOptionName
import com.infendro.cli.parser.Parser import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex.OPTION import com.infendro.cli.util.Regex.OPTION

View File

@@ -1,4 +1,4 @@
package com.infendro.cli.command.option package com.infendro.cli.option
import com.infendro.cli.parser.* import com.infendro.cli.parser.*

View File

@@ -1,7 +1,7 @@
package com.infendro.cli.parser package com.infendro.cli.parser
import com.infendro.cli.error.run.InvalidValue import com.infendro.cli.error.Error
import com.infendro.cli.error.run.RuntimeFailure import com.infendro.cli.error.InvalidValue
import kotlin.reflect.KClass import kotlin.reflect.KClass
abstract class Parser<T : Any>( abstract class Parser<T : Any>(
@@ -13,7 +13,7 @@ abstract class Parser<T : Any>(
) : Result<T>() ) : Result<T>()
data class Failure<T : Any>( data class Failure<T : Any>(
val failure: RuntimeFailure, val error: Error,
) : Result<T>() ) : Result<T>()
} }

View File

@@ -3,7 +3,13 @@ package com.infendro.cli.util
internal const val threshold = 2 internal const val threshold = 2
/** /**
* Damerau-Levenshtein string distance * Calculates the Damerau-Levenshtein distance between two strings.
*
* The distance is the minimum number of operations (insertion, deletion, substitution, and transposition) required to transform one string into another.
*
* @param a The source string.
* @param b The target string.
* @return The minimum number of operations to transform [a] into [b].
*/ */
internal fun distance(a: String, b: String): Int { internal fun distance(a: String, b: String): Int {
if (a == b) return 0 if (a == b) return 0