Initial commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
plugins {
|
||||
id("kotlin-application")
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.slf4j:slf4j-api:2.0.0-alpha1")
|
||||
runtimeOnly("org.slf4j:slf4j-simple:2.0.0-alpha1")
|
||||
implementation("io.pebbletemplates:pebble:3.1.5")
|
||||
implementation("com.github.ajalt.clikt:clikt:3.1.0")
|
||||
implementation("com.electronwill.night-config:toml:3.6.3")
|
||||
}
|
||||
|
||||
application {
|
||||
mainClassName = "scaffold.ScaffoldKt"
|
||||
mainClass.set("scaffold.ScaffoldKt")
|
||||
applicationName = "scaffold"
|
||||
}
|
||||
|
||||
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
|
||||
archiveBaseName.set("scaffold")
|
||||
archiveClassifier.set("")
|
||||
archiveVersion.set("")
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package scaffold
|
||||
|
||||
import com.electronwill.nightconfig.core.Config
|
||||
import com.electronwill.nightconfig.core.UnmodifiableConfig
|
||||
import com.electronwill.nightconfig.core.file.FileConfig
|
||||
import java.nio.file.Path
|
||||
|
||||
data class GeneratorConfig(
|
||||
val templates: List<String>,
|
||||
val files: List<Path>,
|
||||
val prompts: Map<String, List<PromptValue>>
|
||||
)
|
||||
|
||||
fun Generator.loadConfig(): GeneratorConfig {
|
||||
val config: UnmodifiableConfig = FileConfig.of(rootPath.resolve("config.toml")).apply { load() }
|
||||
val templates = config.get<List<String>>("templates")
|
||||
|
||||
val prompts = mutableMapOf<String, MutableList<PromptValue>>()
|
||||
val section = config.get<Any?>("prompt") ?: error("Missing prompt section")
|
||||
if (section !is Config) error("Prompt section is not an object")
|
||||
|
||||
for ((subsectionName, subsection) in section.valueMap()) {
|
||||
check(subsection is List<*>) { "prompts.$subsectionName is not a list of prompts" }
|
||||
|
||||
for (prompt in subsection) {
|
||||
check(prompt is Config)
|
||||
|
||||
val promptValue = PromptValue(
|
||||
name = prompt.get("name") ?: error("Missing name"),
|
||||
info = prompt.get("info"),
|
||||
type = prompt.getEnum("type", PromptType::class.java) ?: PromptType.String
|
||||
)
|
||||
|
||||
val current = prompts.computeIfAbsent(subsectionName) { ArrayList() }
|
||||
current += promptValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val files = config.get<List<String>>("files").flatMap { path ->
|
||||
if ("*" in path) FileGlob.listFiles(treeRoot, path)
|
||||
else listOf(treeRoot.resolve(path))
|
||||
}
|
||||
|
||||
return GeneratorConfig(templates, files, prompts)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package scaffold
|
||||
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
object FileGlob {
|
||||
|
||||
fun listFiles(root: Path, glob: String): MutableList<Path> {
|
||||
val matcher = FileSystems.getDefault().getPathMatcher("glob:$root/$glob")
|
||||
|
||||
val matches = mutableListOf<Path>()
|
||||
|
||||
Files.walkFileTree(root, object : SimpleFileVisitor<Path>() {
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
|
||||
if (matcher.matches(file)) matches.add(file)
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
|
||||
return matches
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package scaffold
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
class Generator(configDirectory: Path, name: String) {
|
||||
val rootPath: Path = configDirectory.resolve(name)
|
||||
val treeRoot: Path = rootPath.resolve("tree")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package scaffold
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.streams.toList
|
||||
|
||||
class Generators(val configDirectory: Path) {
|
||||
fun isValid(name: String): Boolean {
|
||||
val generatorRoot = configDirectory.resolve(name)
|
||||
return Files.isDirectory(generatorRoot) && Files.isRegularFile(generatorRoot.resolve("config.toml"))
|
||||
}
|
||||
|
||||
fun findAll(): List<Path> = Files
|
||||
.list(configDirectory)
|
||||
.filter { Files.exists(it.resolve("config.toml")) }
|
||||
.toList()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package scaffold
|
||||
|
||||
enum class PromptType { String }
|
||||
|
||||
data class PromptValue(
|
||||
val name: String,
|
||||
val info: String?,
|
||||
val type: PromptType,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package scaffold
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.subcommands
|
||||
import scaffold.commands.GenerateCommand
|
||||
import scaffold.commands.ListCommand
|
||||
import scaffold.commands.NewCommand
|
||||
import java.nio.file.Path
|
||||
|
||||
class Scaffold : CliktCommand() {
|
||||
override fun run() {}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val configDirectory = when (System.getProperty("os.name")) {
|
||||
"Linux" -> System.getenv("XDG_CONFIG_HOME")?.let { Path.of(it) }
|
||||
?: Path.of(System.getProperty("user.home"), ".config")
|
||||
else -> TODO("¯\\_(ツ)_/¯")
|
||||
}
|
||||
|
||||
val scaffoldConfigDirectory = configDirectory.resolve("scaffold")
|
||||
val generatorService = Generators(scaffoldConfigDirectory)
|
||||
|
||||
Scaffold().subcommands(
|
||||
ListCommand(generatorService),
|
||||
GenerateCommand(generatorService),
|
||||
NewCommand(generatorService)
|
||||
).main(args)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package scaffold.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.ProgramResult
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.options.convert
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.required
|
||||
import com.mitchellbosecke.pebble.PebbleEngine
|
||||
import com.mitchellbosecke.pebble.loader.FileLoader
|
||||
import com.mitchellbosecke.pebble.template.PebbleTemplate
|
||||
import scaffold.Generator
|
||||
import scaffold.Generators
|
||||
import scaffold.PromptType
|
||||
import scaffold.loadConfig
|
||||
import java.io.StringWriter
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class GenerateCommand(private val generators: Generators) : CliktCommand("generate") {
|
||||
private val name by argument(help = "generator name")
|
||||
private val output by option().convert { Path.of(it) }.required()
|
||||
|
||||
override fun run() {
|
||||
checkPreconditions()
|
||||
|
||||
val generator = Generator(generators.configDirectory, name)
|
||||
val config = generator.loadConfig()
|
||||
|
||||
val templateContext = mutableMapOf<String, Any?>()
|
||||
|
||||
for ((path, prompts) in config.prompts) {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
templateContext[path] = map
|
||||
for ((name, info, type) in prompts) {
|
||||
if (type != PromptType.String) TODO()
|
||||
|
||||
val value = prompt(info ?: name)!!
|
||||
map[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
val pebble = PebbleEngine.Builder()
|
||||
.autoEscaping(false)
|
||||
.newLineTrimming(false)
|
||||
.loader(FileLoader().apply { prefix = generator.treeRoot.toString() })
|
||||
.build()!!
|
||||
|
||||
for (template in config.templates) {
|
||||
val renderedTemplate = pebble.getTemplate(template)(templateContext)
|
||||
val outputPath = output.resolve(template)
|
||||
|
||||
Files.createDirectories(outputPath.parent)
|
||||
Files.writeString(outputPath, renderedTemplate)
|
||||
}
|
||||
|
||||
for (inputFile in config.files) {
|
||||
val relativeFile = generator.treeRoot.relativize(inputFile)
|
||||
val outputFile = output.resolve(relativeFile)
|
||||
|
||||
Files.createDirectories(outputFile.parent)
|
||||
Files.copy(inputFile, outputFile)
|
||||
}
|
||||
|
||||
echo("Generated project in $output")
|
||||
}
|
||||
|
||||
private fun checkPreconditions() {
|
||||
if (Files.exists(output)) {
|
||||
if (Files.list(output).findAny().isPresent) {
|
||||
echo("Output path `$output` is not empty")
|
||||
throw ProgramResult(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!generators.isValid(name)) {
|
||||
echo("Generator not found")
|
||||
throw ProgramResult(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun PebbleTemplate.invoke(context: Map<String, Any?>): String =
|
||||
StringWriter().also { evaluate(it, context) }.toString()
|
||||
@@ -0,0 +1,8 @@
|
||||
package scaffold.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import scaffold.Generators
|
||||
|
||||
class ListCommand(private val generators: Generators) : CliktCommand("list") {
|
||||
override fun run() = echo(generators.findAll().joinToString(" ") { it.last().toString() })
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package scaffold.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.ProgramResult
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import scaffold.Generators
|
||||
import java.nio.file.Files
|
||||
|
||||
class NewCommand(private val generators: Generators) : CliktCommand("new") {
|
||||
private val name by argument()
|
||||
|
||||
override fun run() {
|
||||
val root = generators.configDirectory.resolve(name)
|
||||
|
||||
if (Files.exists(root)) {
|
||||
echo("Generator already exists")
|
||||
throw ProgramResult(1)
|
||||
}
|
||||
|
||||
Files.createDirectories(root)
|
||||
|
||||
val configFile = """
|
||||
templates = [
|
||||
"README.md"
|
||||
]
|
||||
|
||||
files = [
|
||||
".gitignore"
|
||||
]
|
||||
|
||||
[prompt]
|
||||
|
||||
[[prompt.project]]
|
||||
type = "String"
|
||||
name = "name"
|
||||
info = "Project name"
|
||||
""".trimIndent()
|
||||
|
||||
Files.writeString(root.resolve("config.toml"), configFile)
|
||||
val tree = root.resolve("tree")
|
||||
Files.createDirectory(tree)
|
||||
|
||||
Files.writeString(tree.resolve("README.md"), "# {{ project.name }}")
|
||||
Files.writeString(tree.resolve(".gitignore"), "# https://git-scm.com/docs/gitignore")
|
||||
|
||||
echo("Generator created in $root")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user