Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,44 @@ cag [--new-architecture [--no-compose] [--ktlint] [--detekt]]... [--new-feature
```

When run without arguments, the command prints a short usage and suggests using `--help` or `-h` for more options.

### CLI configuration (.cagrc)

You can configure library and plugin versions used by the CLI via a simple INI-style config file named `.cagrc`.

- Locations:
- Project root: `./.cagrc`
- User home: `~/.cagrc`

- Precedence:
- Values in the project `.cagrc` override values in `~/.cagrc`.

- Sections:
- `[new.versions]` — applied when generating new projects (e.g., `--new-project`).
- `[existing.versions]` — applied when generating into an existing project (e.g., new architecture, feature, data source, use case, or view model).

- Keys correspond to version keys used by the generator, for example: `kotlin`, `androidGradlePlugin`, `composeBom`, `composeNavigation`, `retrofit`, `ktor`, `okhttp3`, etc.

Example `~/.cagrc`:

```
[new.versions]
kotlin=2.2.10
composeBom=2025.08.01

[existing.versions]
retrofit=2.11.0
ktor=3.0.3
```

Example `./.cagrc` (project overrides):

```
[new.versions]
composeBom=2025.09.01

[existing.versions]
okhttp3=4.12.0
```

With the above, new projects will use `composeBom=2025.09.01` (from project), `kotlin=2.2.10` (from home). For operations on existing projects, `retrofit=2.11.0` (home) and `okhttp3=4.12.0` (project) will be applied.
15 changes: 15 additions & 0 deletions cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mitteloupe.cag.cli

import com.mitteloupe.cag.cli.configuration.ClientConfigurationLoader
import com.mitteloupe.cag.cli.filesystem.CliFileSystemBridge
import com.mitteloupe.cag.core.DirectoryFinder
import com.mitteloupe.cag.core.GenerationException
Expand All @@ -24,6 +25,7 @@ import com.mitteloupe.cag.core.generation.UiLayerContentGenerator
import com.mitteloupe.cag.core.generation.architecture.ArchitectureModulesContentGenerator
import com.mitteloupe.cag.core.generation.architecture.CoroutineModuleContentGenerator
import com.mitteloupe.cag.core.generation.filesystem.FileCreator
import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogSettingsAccessor
import com.mitteloupe.cag.core.generation.versioncatalog.VersionCatalogUpdater
import com.mitteloupe.cag.core.request.GenerateArchitectureRequest
import com.mitteloupe.cag.core.request.GenerateFeatureRequestBuilder
Expand All @@ -47,6 +49,7 @@ fun main(arguments: Array<String>) {
val projectRoot = findGradleProjectRoot(Paths.get("").toAbsolutePath().toFile()) ?: Paths.get("").toAbsolutePath().toFile()
val projectModel = FilesystemProjectModel(projectRoot)
val basePackage = NamespaceResolver().determineBasePackage(projectModel)
val configuration = ClientConfigurationLoader().load(projectRoot)

if (argumentProcessor.isHelpRequested(arguments)) {
printHelpMessage()
Expand Down Expand Up @@ -102,6 +105,7 @@ fun main(arguments: Array<String>) {
enableRetrofit = request.enableRetrofit
)
executeAndReport {
setVersionProvider(configuration.newProjectVersions)
generator.generateProjectTemplate(projectTemplateRequest)
}
}
Expand All @@ -117,6 +121,7 @@ fun main(arguments: Array<String>) {
enableDetekt = request.enableDetekt
)
executeAndReport {
setVersionProvider(configuration.existingProjectVersions)
generator.generateArchitecture(architectureRequest)
}
}
Expand All @@ -136,12 +141,14 @@ fun main(arguments: Array<String>) {
.enableDetekt(requestFeature.enableDetekt)
.build()
executeAndReport {
setVersionProvider(configuration.existingProjectVersions)
generator.generateFeature(request)
}
}

dataSourceRequests.forEach { request ->
executeAndReport {
setVersionProvider(configuration.existingProjectVersions)
generator.generateDataSource(
destinationRootDirectory = destinationRootDirectory,
dataSourceName = request.dataSourceName,
Expand All @@ -165,6 +172,7 @@ fun main(arguments: Array<String>) {
.build()

executeAndReport {
setVersionProvider(configuration.existingProjectVersions)
generator.generateUseCase(useCaseRequest)
}
}
Expand All @@ -184,6 +192,7 @@ fun main(arguments: Array<String>) {
).build()

executeAndReport {
setVersionProvider(configuration.existingProjectVersions)
generator.generateViewModel(viewModelRequest)
}
}
Expand Down Expand Up @@ -312,3 +321,9 @@ private fun executeAndReport(operation: () -> Unit) =
println("Error: ${exception.message}")
exitProcess(1)
}

private fun setVersionProvider(overrides: Map<String, String>) {
VersionCatalogSettingsAccessor.setProvider { key, default ->
overrides[key] ?: default
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mitteloupe.cag.cli.configuration

data class ClientConfiguration(
val newProjectVersions: Map<String, String> = emptyMap(),
val existingProjectVersions: Map<String, String> = emptyMap()
) {
companion object {
val EMPTY = ClientConfiguration()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.mitteloupe.cag.cli.configuration

import java.io.File

private const val FILE_NAME = ".cagrc"

class ClientConfigurationLoader {
fun load(projectRoot: File): ClientConfiguration {
val projectFile = File(projectRoot, FILE_NAME)
val homeFile = File(System.getProperty("user.home"), FILE_NAME)
return loadFromFiles(projectFile = projectFile, homeFile = homeFile)
}

internal fun loadFromFiles(
projectFile: File?,
homeFile: File?
): ClientConfiguration {
val homeConfiguration = if (homeFile?.exists() == true && homeFile.isFile) parse(homeFile.readText()) else ClientConfiguration.EMPTY
val projectConfiguration =
if (projectFile?.exists() == true && projectFile.isFile) {
parse(projectFile.readText())
} else {
ClientConfiguration.EMPTY
}
return mergeConfigurations(baseConfiguration = homeConfiguration, override = projectConfiguration)
}

private fun mergeConfigurations(
baseConfiguration: ClientConfiguration,
override: ClientConfiguration
): ClientConfiguration =
ClientConfiguration(
newProjectVersions = baseConfiguration.newProjectVersions + override.newProjectVersions,
existingProjectVersions = baseConfiguration.existingProjectVersions + override.existingProjectVersions
)

internal fun parse(text: String): ClientConfiguration {
val newProjectVersions = mutableMapOf<String, String>()
val existingProjectVersions = mutableMapOf<String, String>()

var currentVersionsMap: MutableMap<String, String>? = null

text.lineSequence().forEach { rawLine ->
val line = rawLine.trim()
if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) return@forEach

if (line.startsWith("[") && line.endsWith("]")) {
currentVersionsMap =
when (line.substring(1, line.length - 1).lowercase()) {
"new.versions" -> newProjectVersions
"existing.versions" -> existingProjectVersions
else -> null
}
return@forEach
}

val index = line.indexOf('=')
if (index > 0 && currentVersionsMap != null) {
val key = line.take(index).trim()
val value = line.substring(index + 1).trim()
if (key.isNotEmpty() && value.isNotEmpty()) {
currentVersionsMap[key] = value
}
}
}

return ClientConfiguration(newProjectVersions = newProjectVersions, existingProjectVersions = existingProjectVersions)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.mitteloupe.cag.cli.configuration

import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.File
import kotlin.io.path.createTempDirectory

class VersionConfigLoaderTest {
private lateinit var classUnderTest: ClientConfigurationLoader

@Before
fun setUp() {
classUnderTest = ClientConfigurationLoader()
}

@Test
fun `Given INI text with new and existing sections when parse then parses values`() {
// Given
val text =
"""
# Comment
[new.versions]
kotlin=2.2.99
composeBom=2099.01.01

[existing.versions]
retrofit=2.99.0
ktor=9.9.9
""".trimIndent()
val expectedClientConfiguration =
ClientConfiguration(
newProjectVersions =
mapOf(
"kotlin" to "2.2.99",
"composeBom" to "2099.01.01"
),
existingProjectVersions =
mapOf(
"retrofit" to "2.99.0",
"ktor" to "9.9.9"
)
)

// When
val actualConfiguration = classUnderTest.parse(text)

// Then
assertEquals(expectedClientConfiguration, actualConfiguration)
}

@Test
fun `Given home, project configurations when loadFromFiles then project overrides home`() {
// Given
val temporaryDirectory = createTempDirectory(prefix = "cag-test-").toFile()
try {
val homeFile =
File(temporaryDirectory, "home.cagrc").apply {
writeText(
"""
[new.versions]
composeBom=2000.01.01
ktor=1.0.0
[existing.versions]
retrofit=2.0.0
""".trimIndent()
)
}

val projectFile =
File(temporaryDirectory, ".cagrc").apply {
writeText(
"""
[new.versions]
composeBom=2025.08.01
[existing.versions]
retrofit=2.11.0
okhttp3=4.12.0
""".trimIndent()
)
}
val expectedConfiguration =
ClientConfiguration(
newProjectVersions =
mapOf(
"composeBom" to "2025.08.01",
"ktor" to "1.0.0"
),
existingProjectVersions =
mapOf(
"retrofit" to "2.11.0",
"okhttp3" to "4.12.0"
)
)

// When
val actualConfiguration = classUnderTest.loadFromFiles(projectFile = projectFile, homeFile = homeFile)

// Then
assertEquals(expectedConfiguration, actualConfiguration)
} finally {
temporaryDirectory.deleteRecursively()
}
}
}