Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt committed Nov 22, 2020
0 parents commit b5252b7
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gradle
.idea
build/
.DS_Store
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
![badge][badge-mac]

# Kommand

Quick command execution tool.

## Configuration

Kommand is configured via `$HOME/.kommand.js`. The configuration file has the following structure:

```
{
"home": PROJECTS_ROOT_DIRECTORY,
"projects": {
PROJECT_DIRECTORY: {
COMMAND_NAME: {
"dependsOn": [
{
"project": PROJECT_NAME,
"command": COMMAND_NAME
},
...
],
"command": SHELL_COMMAND
},
...
},
...
}
}
```

When `kommand` is run, the current directory name is considered the **project** being operated on.
Execution working directory will be the current directory name relative to the configured `home`.

### Example

```json
{
"home": "/Users/travis/Projects",
"projects": {
"kable": {
"install": {
"command": "./gradlew publishToMavenLocal"
}
},
"sensortag": {
"run": {
"dependsOn": [
{
"project": "kable",
"command": "install"
}
],
"command": "./gradlew runDebugExecutableMacosX64"
}
}
}
}
```

With the current directory as `/Users/travis/Projects/sensortag`, running `kommand run` will result in:

```
🏃 kable ▶ ./gradlew publishToMavenLocal
🏃 sensortag ▶ ./gradlew runDebugExecutableMacosX64
```


[badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat
[badge-ios]: http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat
[badge-js]: http://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat
[badge-jvm]: http://img.shields.io/badge/platform-jvm-DB413D.svg?style=flat
[badge-linux]: http://img.shields.io/badge/platform-linux-2D3F6C.svg?style=flat
[badge-windows]: http://img.shields.io/badge/platform-windows-4D76CD.svg?style=flat
[badge-mac]: http://img.shields.io/badge/platform-macos-111111.svg?style=flat
[badge-watchos]: http://img.shields.io/badge/platform-watchos-C0C0C0.svg?style=flat
[badge-tvos]: http://img.shields.io/badge/platform-tvos-808080.svg?style=flat
[badge-wasm]: https://img.shields.io/badge/platform-wasm-624FE8.svg?style=flat
16 changes: 16 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
buildscript {
repositories {
jcenter()
}
}

plugins {
kotlin("multiplatform") version "1.4.20" apply false
kotlin("plugin.serialization") version "1.4.20" apply false
}

subprojects {
repositories {
jcenter()
}
}
24 changes: 24 additions & 0 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
}

kotlin {
macosX64 {
binaries {
executable {
baseName = "kommand"
entryPoint = "com.juul.kommand.main"
}
}
}

sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("com.github.ajalt.clikt:clikt:3.0.1")
}
}
}
}
66 changes: 66 additions & 0 deletions cli/src/commonMain/kotlin/Config.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.juul.kommand

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json

expect fun read(path: String): String?

val config: Config by lazy {
val configPath = "$homePath/.kommand.json"
val text = read(configPath) ?: error("Config '$configPath' not found")
Json.Default.decodeFromString(text)
}

private typealias Name = String

@Serializable(with = ConfigSerializer::class)
data class Config(
@SerialName("home") val homeDirectory: String,
val projects: Map<Name, Project>,
)

data class Project(
val name: String,
val commands: Map<Name, Command>,
)

@Serializable
data class Command(
@SerialName("dependsOn") val dependencies: List<Dependency>? = null,
val command: String
)

@Serializable
data class Dependency(
val project: String,
val command: String,
)

object ConfigSerializer : KSerializer<Config> {

override val descriptor: SerialDescriptor = ConfigSurrogate.serializer().descriptor

override fun serialize(encoder: Encoder, value: Config) = throw UnsupportedOperationException()

override fun deserialize(decoder: Decoder): Config {
val surrogate = decoder.decodeSerializableValue(ConfigSurrogate.serializer())
return Config(
homeDirectory = surrogate.homeDirectory,
projects = surrogate.projects.map { (name, commands) ->
name to Project(name, commands)
}.toMap()
)
}
}

@Serializable
private data class ConfigSurrogate(
@SerialName("home") val homeDirectory: String,
val projects: Map<Name, Map<Name, Command>>,
)
3 changes: 3 additions & 0 deletions cli/src/commonMain/kotlin/Execute.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.juul.kommand

expect fun execute(path: String, command: String)
44 changes: 44 additions & 0 deletions cli/src/commonMain/kotlin/Kommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.juul.kommand

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option

class Kommand : CliktCommand() {

private val command by argument()
private val dryRun by option("-d", "--dry-run").flag()

override fun run() {
val projectName = currentDirectoryPath.substringAfterLast("/")
val project = config.projects[projectName]
?: error("Project '$projectName' not found in config")

project.execute(
command.let { project.commands[it] ?: error("Command '$it' not found in project") },
dryRun
)
}
}

fun Config.path(project: Project) = "$homeDirectory/${project.name}"

fun Project.execute(command: Command, dryRun: Boolean) {
command.dependencies?.forEach { execute(it, dryRun) }
if (!dryRun) println()
println("🏃 $name${command.command}")
if (!dryRun) execute(config.path(this), command.command)
}

fun execute(dependency: Dependency, dryRun: Boolean) {
val projectName = dependency.project
val commandName = dependency.command

val project = config.projects[projectName]
?: error("Project '$projectName' not found in config")
val command = project.commands[commandName]
?: error("Command '$commandName' not found in project '$projectName'")

project.execute(command, dryRun)
}
4 changes: 4 additions & 0 deletions cli/src/commonMain/kotlin/Paths.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.juul.kommand

expect val currentDirectoryPath: String
expect val homePath: String
8 changes: 8 additions & 0 deletions cli/src/macosX64Main/kotlin/Config.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.juul.kommand

import platform.Foundation.NSString
import platform.Foundation.stringWithContentsOfFile

actual fun read(
path: String
): String? = NSString.stringWithContentsOfFile(path) as? String
22 changes: 22 additions & 0 deletions cli/src/macosX64Main/kotlin/Execute.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.juul.kommand

import kotlinx.cinterop.autoreleasepool
import platform.Foundation.NSPipe
import platform.Foundation.NSTask
import platform.Foundation.currentDirectoryPath
import platform.Foundation.launch
import platform.Foundation.launchPath
import platform.Foundation.waitUntilExit

actual fun execute(path: String, command: String) {
autoreleasepool {
val stdin = NSPipe()
val task = NSTask()
task.currentDirectoryPath = path
task.launchPath = "/bin/sh"
task.arguments = listOf("-c", command)
task.standardInput = stdin
task.launch()
task.waitUntilExit()
}
}
9 changes: 9 additions & 0 deletions cli/src/macosX64Main/kotlin/Paths.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.juul.kommand

import platform.Foundation.NSFileManager
import platform.Foundation.homeDirectoryForCurrentUser

actual val currentDirectoryPath: String = NSFileManager.defaultManager.currentDirectoryPath

actual val homePath: String = NSFileManager.defaultManager.homeDirectoryForCurrentUser.path
?: error("Home directory unavailable")
14 changes: 14 additions & 0 deletions cli/src/macosX64Main/kotlin/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.juul.kommand

fun main(args: Array<String>) {
if (args.isEmpty()) printCommands()
Kommand().main(args)
}

private fun printCommands() {
val projectName = currentDirectoryPath.substringAfterLast("/")
val project = config.projects[projectName] ?: return
println("$projectName COMMANDs")
project.commands.keys.forEach { println("$it") }
println()
}
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
5 changes: 5 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit b5252b7

Please sign in to comment.