Repository: OneLiteFeatherNET/Stelaris-CLI
Pull request #7: Revise how the cli handles the generation and exportation of the generated files
Author: login: theEvilReaper
Labels: name: enhancement
description: New feature or request
name: Feature
Created: 2025-03-09T16:46:16Z | Merged: 2025-03-15T10:55:32Z

## Overview

This pull request updates how the CLI handles exporting generated files. It allows now switching between a `local` and `git` build. Local exports the files to a specified folder, while git pushes them to a configured repository

### Submitter Checklist
- [x] Make sure you are opening from a topic branch (**/feature/fix/docs/ branch** (right side)) and not your main branch.
- [x] Ensure that the pull request title represents the desired changelog entry.
- [ ] New public fields and methods are annotated with `@since TODO`.
- [x] I read and followed the [contribution guidelines



Diff:
diff --git a/build.gradle.kts b/build.gradle.kts
index 26731db..2aed3b1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,6 +18,7 @@ repositories {
 dependencies {
     implementation(platform(libs.microtus.bom))
     implementation(libs.dartpoet)
+    implementation(libs.snakeyaml)
     implementation(libs.jetbrains.annotation)
     implementation(libs.microtus)
     implementation(libs.guava)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 938e6c6..7297a41 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,7 @@ dependencyResolutionManagement {
             library("junit.jupiter.params", "org.junit.jupiter", "junit-jupiter-params").versionRef("junit")
             library("junit.jupiter.engine", "org.junit.jupiter", "junit-jupiter-engine").versionRef("junit")
             library("junit.platform.launcher", "org.junit.platform", "junit-platform-launcher").version("1.12.0")
+            library("snakeyaml", "org.yaml", "snakeyaml").version("2.4")
             plugin("kotlin", "org.jetbrains.kotlin.jvm").versionRef("kotlin")
         }
     }
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
index 8dc32cc..490b4c3 100644
--- a/src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
@@ -1,13 +1,17 @@
 package net.theevilreaper.stelaris.cli
 
 import net.minestom.server.MinecraftServer
+import net.theevilreaper.stelaris.cli.arguments.CommandArgument
+import net.theevilreaper.stelaris.cli.arguments.ParsedArgs
+import net.theevilreaper.stelaris.cli.exporter.ExportStrategy
+import net.theevilreaper.stelaris.cli.exporter.GitProjectExporter
+import net.theevilreaper.stelaris.cli.exporter.LocalProjectExporter
 import net.theevilreaper.stelaris.cli.generator.Generator
 import net.theevilreaper.stelaris.cli.generator.GeneratorRegistry
 import net.theevilreaper.stelaris.cli.util.*
-import org.eclipse.jgit.lib.PersonIdent
-import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
 import java.nio.file.Files
 import java.nio.file.Path
+import kotlin.system.exitProcess
 
 fun main(args: Array<String>) {
     if (args.isEmpty()) {
@@ -16,9 +20,52 @@ fun main(args: Array<String>) {
     }
 
     val generatorRegistry = GeneratorRegistry()
+    val parsedArgs = parseArguments(args)
+
+    if (parsedArgs.showHelp) {
+        println(HELP_MESSAGE)
+        return
+    }
+
+    if (parsedArgs.versionPart == null) {
+        println("The version part is required")
+        exitProcess(1)
+        return
+    }
+
+    val generators: Set<Generator> = when (parsedArgs.experimental) {
+        false -> generatorRegistry.getGenerators { !it.isExperimental() }
+        true -> generatorRegistry.getGenerators()
+    }
+
+    if (generators.isEmpty()) {
+        println("The cli needs generators to run")
+        exitProcess(1)
+        return
+    }
+
+    // Use the user-specified path if provided; otherwise, use the default for Git
+    val workingDir = when {
+        parsedArgs.localBuild -> parsedArgs.path ?: Files.createTempDirectory(TEMP_DIR_NAME)
+        else -> Files.createTempDirectory(TEMP_DIR_NAME)
+    }
+
+    MinecraftServer.init()
+
+    val projectExporter = when (parsedArgs.localBuild) {
+        true -> LocalProjectExporter(workingDir, "", generators)
+        false -> GitProjectExporter(workingDir, "", versionPart = parsedArgs.versionPart, generators)
+    }
+
+    projectExporter.export()
+}
+
+private fun parseArguments(args: Array<String>): ParsedArgs {
     var showHelp = false
     var versionPart: VersionPart? = null
-    var experimental: Boolean = false
+    var experimental = false
+    var localBuild = true
+    var path: Path? = null
 
     args.forEachIndexed { index, arg ->
         if (arg.startsWith(ARGUMENT_IDENTIFIER)) {
@@ -40,58 +87,25 @@ fun main(args: Array<String>) {
                         println("A version part can't start with an $ARGUMENT_IDENTIFIER")
                         return@forEachIndexed
                     }
-
                     versionPart = VersionPart.parse(versionPartString)
                 }
 
                 CommandArgument.EXPERIMENTAL -> experimental = true
+                CommandArgument.TYPE -> {
+                    val type = args[index + 1]
+                    val exportStrategy = ExportStrategy.fromIdentifier(type)
+                    if (exportStrategy == ExportStrategy.GIT) {
+                        localBuild = false
+                    }
+                }
+
+                CommandArgument.PATH -> {
+                    val pathString = args[index + 1]
+                    path = Path.of(pathString)
+                }
             }
         }
     }
 
-    if (showHelp) {
-        println(HELP_MESSAGE)
-    }
-
-    val generators: Set<Generator> = when(experimental) {
-        false -> generatorRegistry.getGenerators { !it.isExperimental() }
-        true -> generatorRegistry.getGenerators()
-    }
-
-    if (generators.isEmpty()) {
-        println("The cli needs generators to run")
-        return
-    }
-
-    val tempFile: Path = Files.createTempDirectory(TEMP_DIR_NAME)
-
-    MinecraftServer.init();
-
-    generators.forEach { generator -> generator.generate(tempFile) }
-
-    val gitRepo = cloneBaseRepo(
-        System.getenv("stelaris.cli.username"),
-        System.getenv("stelaris.cli.password"),
-        System.getenv("stelaris.cli.cloneUrl"),
-        tempFile
-    )
-
-    gitRepo.add().addFilepattern(".").call()
-    val commit = gitRepo.commit()
-    commit.message = "Update version part: ${versionPart!!.part}"
-    commit.setAuthor(PersonIdent("Stelaris CLI", "gitlab+generator@onelitefeather.net"))
-    commit.setAll(true)
-    commit.call()
-
-    val gitPush = gitRepo.push()
-
-    gitPush.setCredentialsProvider(
-        UsernamePasswordCredentialsProvider(
-            System.getenv("stelaris.cli.username"),
-            System.getenv("stelaris.cli.password")
-        )
-    )
-
-    gitPush.isForce = true
-    gitPush.call()
+    return ParsedArgs(showHelp, versionPart, experimental, localBuild, path)
 }
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgument.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/CommandArgument.kt
similarity index 82%
rename from src/main/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgument.kt
rename to src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/CommandArgument.kt
index 212d2f5..34d76cb 100644
--- a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgument.kt
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/CommandArgument.kt
@@ -1,4 +1,4 @@
-package net.theevilreaper.stelaris.cli.util
+package net.theevilreaper.stelaris.cli.arguments
 
 /**
  * Represents the different command arguments which can be used.
@@ -12,7 +12,10 @@ enum class CommandArgument(val identifier: String, val helpMessage: String) {
 
     HELP("help", "Shows this help message"),
     UPDATE("update", "Updates a specific part of the version string"),
-    EXPERIMENTAL("experimental", "Enables experimental generators");
+    EXPERIMENTAL("experimental", "Enables experimental generators"),
+    TYPE("type", "The generation type"),
+    PATH("path", "The path to the project")
+    ;
 
     companion object {
 
@@ -20,4 +23,4 @@ enum class CommandArgument(val identifier: String, val helpMessage: String) {
             return entries.find { it.identifier == identifier }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgs.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgs.kt
new file mode 100644
index 0000000..d7b24f8
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgs.kt
@@ -0,0 +1,25 @@
+package net.theevilreaper.stelaris.cli.arguments
+
+import net.theevilreaper.stelaris.cli.util.VersionPart
+import java.nio.file.Path
+
+/**
+ * Data class representing the parsed command-line arguments.
+ *
+ * @property showHelp Indicates whether the help message should be displayed.
+ * @property versionPart The version part specified by the user, or null if not provided.
+ * @property experimental Indicates whether experimental features are enabled.
+ * @property localBuild Indicates whether the local build option is selected.
+ * @property path The path specified by the user, or null if not provided.
+ *
+ * @since 1.0.0
+ * @version 1.0.0
+ * @author theEvilReaper
+ */
+data class ParsedArgs(
+    val showHelp: Boolean,
+    val versionPart: VersionPart?,
+    val experimental: Boolean,
+    val localBuild: Boolean,
+    val path: Path?
+)
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/BaseExporter.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/BaseExporter.kt
new file mode 100644
index 0000000..020fb4a
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/BaseExporter.kt
@@ -0,0 +1,77 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+import org.yaml.snakeyaml.DumperOptions
+import org.yaml.snakeyaml.Yaml
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.StandardCopyOption
+
+/**
+ * The [BaseExporter] class is an additional abstraction layer for exporting projects.
+ * It provides some basic functionality for exporting a project to a specific location.
+ * @author theEvilReaper
+ * @since 1.0.0
+ * @version 1.0.0
+ * @property pubSpec the name of the pubspec.yaml file
+ */
+abstract class BaseExporter protected constructor() : ProjectExporter {
+
+    private val pubSpec: String = "pubspec.yaml"
+    protected val templateDir = "template"
+    protected val templatePath: Path
+
+    init {
+        val templateDirPath = {}::class.java.classLoader.getResource(templateDir)?.toURI()
+            ?: throw IllegalStateException("Could not find template directory")
+        templatePath = Paths.get(templateDirPath)
+
+    }
+
+    /**
+     * Modify the pubspec.yaml file in the given source folder to the new version.
+     * @param sourceFolder The source folder containing the pubspec.yaml file
+     * @param newVersion The new version to set in the pubspec.yaml file
+     */
+    protected fun modifyPubSpecFile(sourceFolder: Path, newVersion: String) {
+        val pubSpecPath = sourceFolder.resolve(pubSpec)
+        check(Files.exists(pubSpecPath)) { "Could not find $pubSpec.yaml in $sourceFolder" }
+
+        val content = String(Files.readAllBytes(pubSpecPath))
+
+        // Configure YAML options for formatting
+        val options = DumperOptions()
+        options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
+        options.isPrettyFlow = true
+
+        val yaml = Yaml(options)
+
+        // Parse the YAML content to a mutable map
+        @Suppress("UNCHECKED_CAST")
+        val pubspecMap = yaml.load(content) as MutableMap<String, Any>
+
+        // Modify the version
+        pubspecMap["version"] = newVersion
+
+        // Write the modified content back to the file
+        Files.write(pubSpecPath, yaml.dump(pubspecMap).toByteArray())
+    }
+
+    /**
+     * Copy the content of the source folder to the destination folder.
+     * @param sourceFolder The source folder to copy
+     * @param destinationFolder The destination folder to copy the content to
+     */
+    protected fun copyResourceFolder(sourceFolder: Path, destinationFolder: Path) {
+        Files.list(sourceFolder).use { paths ->
+            paths.forEach { path ->
+                val destinationPath = destinationFolder.resolve(path.fileName)
+                if (Files.isDirectory(path)) {
+                    copyResourceFolder(path, destinationPath) // Pass the correct subfolder source
+                } else {
+                    Files.copy(path, destinationPath, StandardCopyOption.REPLACE_EXISTING)
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategy.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategy.kt
new file mode 100644
index 0000000..7118dde
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategy.kt
@@ -0,0 +1,29 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+/**
+ * Represents the different strategies for exporting a project.
+ * @author theEvilReaper
+ * @version 1.0.0
+ * @since 1.0.0
+ * @property identifier the identifier of the strategy
+ */
+enum class ExportStrategy(
+    val identifier: String,
+) {
+    LOCAL("local"),
+    GIT("git")
+
+    ;
+
+    companion object {
+
+        /**
+         * Returns the export strategy from the given identifier.
+         * @param identifier the identifier of the strategy
+         * @return the export strategy or [LOCAL] if the identifier is unknown
+         */
+        fun fromIdentifier(identifier: String): ExportStrategy {
+            return entries.find { it.identifier == identifier } ?: LOCAL
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/GitProjectExporter.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/GitProjectExporter.kt
new file mode 100644
index 0000000..44223cc
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/GitProjectExporter.kt
@@ -0,0 +1,77 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+import net.theevilreaper.stelaris.cli.generator.Generator
+import net.theevilreaper.stelaris.cli.util.VersionPart
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.lib.PersonIdent
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
+import java.nio.file.Path
+
+class GitProjectExporter(
+    private val generationFolder: Path,
+    private val versionString: String,
+    private val versionPart: VersionPart,
+    private val generators: Set<Generator>
+) : BaseExporter() {
+
+    private val userName: String
+    private val password: String
+    private val cloneUrl: String
+
+    init {
+        require((versionString.isNotEmpty())) { "The version string can't be empty" }
+
+        userName = System.getenv("stelaris.cli.username")
+        password = System.getenv("stelaris.cli.password")
+        cloneUrl = System.getenv("stelaris.cli.cloneUrl")
+
+        require((userName.isNotEmpty())) { "The username can't be empty" }
+        require((password.isNotEmpty())) { "The password can't be empty" }
+        require((cloneUrl.isNotEmpty())) { "The clone url can't be empty" }
+    }
+
+    override fun export() {
+        val gitRepo = cloneBaseRepo(
+            generationFolder
+        )
+
+        gitRepo.add().addFilepattern(".").call()
+        val commit = gitRepo.commit()
+        commit.message = "Update version part: ${versionPart.part}"
+        commit.author = PersonIdent("Stelaris CLI", "gitlab+generator@onelitefeather.net")
+        commit.setAll(true)
+        commit.call()
+
+        val gitPush = gitRepo.push()
+
+        gitPush.setCredentialsProvider(
+            UsernamePasswordCredentialsProvider(
+                userName,
+                password
+            )
+        )
+
+        gitPush.isForce = true
+        gitPush.call()
+    }
+
+    /**
+     * Clones the base repository to the given path.
+     * @param username the username for the repository
+     * @param token the token for the repository
+     * @param cloneUrl the clone URL for the repository
+     * @param temp the temporary path to store the repository
+     * @return the cloned repository
+     */
+    private fun cloneBaseRepo(temp: Path): Git {
+        require(cloneUrl.trim().isNotEmpty()) { "Clone URL must not be empty" }
+        val rawGit =
+            Git.cloneRepository().setCredentialsProvider(
+                UsernamePasswordCredentialsProvider(
+                    userName,
+                    password
+                )
+            ).setURI(cloneUrl).setDirectory(temp.toFile()).setCloneAllBranches(true)
+        return rawGit.call()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporter.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporter.kt
new file mode 100644
index 0000000..fa9a754
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporter.kt
@@ -0,0 +1,34 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+import net.theevilreaper.stelaris.cli.generator.Generator
+import java.nio.file.Files
+import java.nio.file.Path
+
+class LocalProjectExporter(
+    private val exportPath: Path,
+    private val versionString: String,
+    private val generators: Set<Generator>
+) : BaseExporter() {
+
+    init {
+        require((versionString.isNotEmpty())) { "The version string can't be empty" }
+    }
+
+    override fun export() {
+        println("Exporting to local path: $exportPath")
+
+        if (!Files.exists(exportPath)) {
+            Files.createDirectories(exportPath)
+            println("Created directory: $exportPath")
+        }
+
+        copyResourceFolder(templatePath, exportPath)
+
+        val libPath: Path = exportPath.resolve("lib")
+        Files.createDirectory(libPath)
+        modifyPubSpecFile(exportPath, versionString)
+
+        generators.forEach { generator -> generator.generate(libPath) }
+
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ProjectExporter.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ProjectExporter.kt
new file mode 100644
index 0000000..3457635
--- /dev/null
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/exporter/ProjectExporter.kt
@@ -0,0 +1,15 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+/**
+ * Represents the interface for exporting a project.
+ * @author theEvilReaper
+ * @since 1.0.0
+ * @version 1.0.0
+ */
+sealed interface ProjectExporter {
+
+    /**
+     * Exports the project to the given location.
+     */
+    fun export()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/Constants.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/util/Constants.kt
index f982257..f540da7 100644
--- a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/Constants.kt
+++ b/src/main/kotlin/net/theevilreaper/stelaris/cli/util/Constants.kt
@@ -1,5 +1,7 @@
 package net.theevilreaper.stelaris.cli.util
 
+import net.theevilreaper.stelaris.cli.arguments.CommandArgument
+
 const val EMPTY_STRING: String = ""
 const val SPACE_STRING: String = ""
 
diff --git a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/GitHelper.kt b/src/main/kotlin/net/theevilreaper/stelaris/cli/util/GitHelper.kt
deleted file mode 100644
index b8a5828..0000000
--- a/src/main/kotlin/net/theevilreaper/stelaris/cli/util/GitHelper.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.theevilreaper.stelaris.cli.util
-
-import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
-import java.nio.file.Path
-
-fun cloneBaseRepo(username: String, token: String, cloneUrl: String, temp: Path): Git {
-    require(username.trim().isNotEmpty()) { "Username must not be empty" }
-    require(token.trim().isNotEmpty()) { "Token must not be empty" }
-    require(cloneUrl.trim().isNotEmpty()) { "Clone URL must not be empty" }
-    val rawGit =
-        Git.cloneRepository().setCredentialsProvider(
-            UsernamePasswordCredentialsProvider(
-                username,
-                token
-            )
-        ).setURI(cloneUrl).setDirectory(temp.toFile()).setCloneAllBranches(true)
-    return rawGit.call()
-}
\ No newline at end of file
diff --git a/src/test/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgsTest.kt b/src/test/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgsTest.kt
new file mode 100644
index 0000000..8a31d2c
--- /dev/null
+++ b/src/test/kotlin/net/theevilreaper/stelaris/cli/arguments/ParsedArgsTest.kt
@@ -0,0 +1,100 @@
+package net.theevilreaper.stelaris.cli.arguments
+
+import net.theevilreaper.stelaris.cli.util.VersionPart
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.nio.file.Paths
+
+class ParsedArgsTest {
+
+    @Test
+    fun `test ParsedArgs creation with all parameters`() {
+        val parsedArgs = ParsedArgs(
+            showHelp = true,
+            versionPart = VersionPart.parse("minor"),
+            experimental = true,
+            localBuild = false,
+            path = Paths.get("/some/path")
+        )
+
+        assertEquals(true, parsedArgs.showHelp)
+        assertEquals(VersionPart.parse("minor"), parsedArgs.versionPart)
+        assertEquals(true, parsedArgs.experimental)
+        assertEquals(false, parsedArgs.localBuild)
+        assertEquals(Paths.get("/some/path"), parsedArgs.path)
+    }
+
+    @Test
+    fun `test ParsedArgs creation with default values`() {
+        val parsedArgs = ParsedArgs(
+            showHelp = false,
+            versionPart = null,
+            experimental = false,
+            localBuild = true,
+            path = null
+        )
+
+        assertEquals(false, parsedArgs.showHelp)
+        assertEquals(null, parsedArgs.versionPart)
+        assertEquals(false, parsedArgs.experimental)
+        assertEquals(true, parsedArgs.localBuild)
+        assertEquals(null, parsedArgs.path)
+    }
+
+    @Test
+    fun `test ParsedArgs with only essential parameters`() {
+        val parsedArgs = ParsedArgs(
+            showHelp = false,
+            versionPart = null,
+            experimental = false,
+            localBuild = true,
+            path = null
+        )
+
+        assertEquals(false, parsedArgs.showHelp)
+        assertEquals(null, parsedArgs.versionPart)
+        assertEquals(false, parsedArgs.experimental)
+        assertEquals(true, parsedArgs.localBuild)
+        assertEquals(null, parsedArgs.path)
+    }
+
+    @Test
+    fun `test ParsedArgs equality`() {
+        val args1 = ParsedArgs(
+            showHelp = true,
+            versionPart = VersionPart.parse("rev"),
+            experimental = false,
+            localBuild = true,
+            path = Paths.get("/path")
+        )
+        val args2 = ParsedArgs(
+            showHelp = true,
+            versionPart = VersionPart.parse("rev"),
+            experimental = false,
+            localBuild = true,
+            path = Paths.get("/path")
+        )
+
+        assertEquals(args1, args2)
+    }
+
+    @Test
+    fun `test ParsedArgs inequality`() {
+        val args1 = ParsedArgs(
+            showHelp = true,
+            versionPart = VersionPart.parse("minor"),
+            experimental = false,
+            localBuild = true,
+            path = Paths.get("/path")
+        )
+        val args2 = ParsedArgs(
+            showHelp = false,
+            versionPart = VersionPart.parse("patch"),
+            experimental = true,
+            localBuild = false,
+            path = Paths.get("/different/path")
+        )
+
+        assertEquals(false, args1 == args2)
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategyTest.kt b/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategyTest.kt
new file mode 100644
index 0000000..06f3964
--- /dev/null
+++ b/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/ExportStrategyTest.kt
@@ -0,0 +1,24 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+class ExportStrategyTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = ["local", "git"])
+    fun `test from identifier`(identifier: String) {
+        val strategy = ExportStrategy.fromIdentifier(identifier)
+        assertNotNull(strategy)
+        assertEquals(identifier, strategy.identifier)
+    }
+
+    @Test
+    fun `test from unknown identifier`() {
+        val strategy = ExportStrategy.fromIdentifier("unknown")
+        assertNotNull(strategy)
+        assertEquals("local", strategy.identifier)
+    }
+}
diff --git a/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporterTest.kt b/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporterTest.kt
new file mode 100644
index 0000000..02063fc
--- /dev/null
+++ b/src/test/kotlin/net/theevilreaper/stelaris/cli/exporter/LocalProjectExporterTest.kt
@@ -0,0 +1,17 @@
+package net.theevilreaper.stelaris.cli.exporter
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import java.nio.file.Paths
+
+class LocalProjectExporterTest {
+
+    @Test
+    fun `test invalid version usage`() {
+        assertThrowsExactly(
+            IllegalArgumentException::class.java,
+            { LocalProjectExporter(Paths.get(""), "", setOf()) },
+            "The version must not be null or empty"
+        )
+    }
+}
diff --git a/src/test/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgumentTest.kt b/src/test/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgumentTest.kt
index 5b2e828..060a037 100644
--- a/src/test/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgumentTest.kt
+++ b/src/test/kotlin/net/theevilreaper/stelaris/cli/util/CommandArgumentTest.kt
@@ -1,5 +1,6 @@
 package net.theevilreaper.stelaris.cli.util
 
+import net.theevilreaper.stelaris.cli.arguments.CommandArgument
 import org.junit.jupiter.api.Assertions.*
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ValueSource


Reviews:
user: login: TheMeinerLP
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T19:00:45Z
user: login: theEvilReaper
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T19:55:54Z
user: login: TheMeinerLP
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T11:51:34Z
user: login: TheMeinerLP
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T21:28:12Z

Review comments:
diff_hunk: @@ -16,9 +20,51 @@ fun main(args: Array<String>) {
     }
 
     val generatorRegistry = GeneratorRegistry()
+    val parsedArgs = parseArguments(args)
+
+    if (parsedArgs.showHelp) {
+        println(HELP_MESSAGE)
path: src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
user: login: TheMeinerLP
body: Why is here the return missing ?
created_at: 2025-03-13T18:59:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,9 +20,51 @@ fun main(args: Array<String>) {
     }
 
     val generatorRegistry = GeneratorRegistry()
+    val parsedArgs = parseArguments(args)
+
+    if (parsedArgs.showHelp) {
+        println(HELP_MESSAGE)
path: src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
user: login: theEvilReaper
body: There should be no return statement to prevent the execution of the CLI. This is a general case where the `help` command is applied as an argument along with other options. The message must be printed here without a return.

I mean we can add a return here but for what?
created_at: 2025-03-13T19:55:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,9 +20,51 @@ fun main(args: Array<String>) {
     }
 
     val generatorRegistry = GeneratorRegistry()
+    val parsedArgs = parseArguments(args)
+
+    if (parsedArgs.showHelp) {
+        println(HELP_MESSAGE)
path: src/main/kotlin/net/theevilreaper/stelaris/cli/StelarisCLI.kt
user: login: TheMeinerLP
body: For just print help like in linux
created_at: 2025-03-14T11:51:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: Joltras
created_at: 2025-03-09T18:18:03Z
body: We should add an argument for specifying the folder.
author_association: CONTRIBUTOR
user: login: theEvilReaper
created_at: 2025-03-09T18:58:17Z
body: There is now a `-path` option which can be used to specify a custom directory for the generation
author_association: CONTRIBUTOR