diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0bb528c..854952e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -36,6 +36,17 @@ jobs: - name: Run check task run: ./gradlew check + - name: Generate CLI man page + run: ./gradlew :cli:generateManPage --no-configuration-cache + + - name: Upload CLI man page + uses: actions/upload-artifact@v4 + with: + name: cli-manpage + path: | + cli/build/man/cag.1 + retention-days: 30 + - name: Upload test results uses: actions/upload-artifact@v4 if: always() diff --git a/README.md b/README.md index 8b6ab46..8017789 100644 --- a/README.md +++ b/README.md @@ -44,73 +44,93 @@ java -jar "cli/build/libs/cli-all.jar" --new-feature --name=MyFeature java -jar "cli/build/libs/cli-all.jar" --new-view-model --name=MyViewModel ``` -#### Options +#### Usage and help -Usage: +Usage (canonical): ```bash -cag [--new-architecture [--no-compose] [--ktlint] [--detekt]]... [--new-feature --name=FeatureName [--package=PackageName]]... [--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... [--new-use-case --name=UseCaseName [--path=TargetPath]]... [--new-view-model --name=ViewModelName [--path=TargetPath]]... +cag [--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit]]... [--new-architecture [--no-compose] [--ktlint] [--detekt]]... [--new-feature --name=FeatureName [--package=PackageName]]... [--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... [--new-use-case --name=UseCaseName [--path=TargetPath]]... [--new-view-model --name=ViewModelName [--path=TargetPath]]... ``` -##### New Architecture Options +- Full reference: `cag --help` +- Topic help: `cag --help --topic=new-feature` or `cag --help -t new-use-case` +- Man page: `man cag` (see below for generating/installing locally) + +Common examples: + ```bash - --new-architecture | -na - Generate a new Clean Architecture package with domain, presentation, and UI layers - --no-compose | -nc - Disable Compose support for the preceding architecture package - --ktlint | -kl - Enable ktlint for the preceding architecture package - --detekt | -d - Enable detekt for the preceding architecture package +# Generate a new project +cag --new-project --name=MyApp --package=com.example.myapp + +# Add architecture to an existing project/module +cag --new-architecture --ktlint --detekt + +# Add a new feature +cag --new-feature --name=Profile --package=com.example.feature.profile + +# Add a data source with Retrofit +cag --new-datasource --name=User --with=retrofit + +# Add a use case +cag --new-use-case --name=FetchUser --path=architecture/domain/src/main/kotlin + +# Add a ViewModel +cag --new-view-model --name=Profile ``` -##### New Feature Options +Manual page (optional): + ```bash - --new-feature --name= | --new-feature --name | -nf --name= | -nf --name - Generate a new feature named - --package= | --package | -p= | -p | -p - (Optional) Override the feature package for the preceding feature +# Generate man page (writes cli/build/man/cag.1) +./gradlew :cli:generateManPage + +# Install to a man1 directory (may require sudo for system directories) +./gradlew :cli:installManPage + +# Preview after install +man cag ``` -##### New DataSource Options -```bash - --new-datasource --name= | --new-datasource --name | -nds --name= | -nds --name - Generate a new DataSource named DataSource - --with=ktor|retrofit|ktor,retrofit | -w=ktor|retrofit|ktor,retrofit - Attach dependencies to the preceding new data source +### 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 -##### New UseCase Options -```bash - --new-use-case --name= | --new-use-case --name | -nuc --name= | -nuc --name - Generate a new use case named UseCase. - --path= | --path | -p= | -p | -p - (Optional) Specify the target directory for the preceding use case - By default, the target path is determined by the current location - --input-type= | --input-type | -it= | -it | -it - (Optional) Specify the input data type for the preceding use case - By default, Unit is used - --output-type= | --output-type | -ot= | -ot | -ot - (Optional) Specify the output data type for the preceding use case - By default, Unit is used +[existing.versions] +retrofit=2.11.0 +ktor=3.0.3 ``` -##### New ViewModel Options -```bash - --new-view-model --name= | --new-view-model --name | -nvm --name= | -nvm --name - Generate a new ViewModel named ViewModel. - --path= | --path | -p= | -p | -p - (Optional) Specify the target directory for the preceding ViewModel - By default, the target path is determined by the current location +Example `./.cagrc` (project overrides): + ``` +[new.versions] +composeBom=2025.09.01 -##### Other Options -```bash - --help, -h - Show the help document for cag +[existing.versions] +okhttp3=4.12.0 ``` -When run without arguments, the command prints a short usage and suggests using `--help` or `-h` for more options. +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. ### CLI configuration (.cagrc) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index ce87033..940daf9 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -45,3 +45,5 @@ ktlint { include("**/*.kts") } } + +apply(from = "man.gradle.kts") diff --git a/cli/man.gradle.kts b/cli/man.gradle.kts new file mode 100644 index 0000000..50d0d54 --- /dev/null +++ b/cli/man.gradle.kts @@ -0,0 +1,196 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.bundling.Jar +import org.gradle.process.ExecOperations +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.file.Paths +import java.util.zip.GZIPOutputStream +import javax.inject.Inject + +abstract class GenerateManPage + @Inject + constructor() : DefaultTask() { + @get:Inject + protected abstract val execOperations: ExecOperations + + @get:Inject + protected abstract val layout: ProjectLayout + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @get:InputFile + abstract val shadowJarFile: RegularFileProperty + + init { + description = "Generates a man page at build/man/cag.1" + group = "documentation" + outputFile.convention(layout.buildDirectory.file("man/cag.1")) + } + + @TaskAction + fun generate() { + val manFile = outputFile.get().asFile + val manDir = manFile.parentFile + if (!manDir.exists()) manDir.mkdirs() + manFile.writeText("") + + execOperations.exec { + commandLine( + "java", + "-jar", + shadowJarFile.get().asFile.absolutePath, + "--help", + "--format=man" + ) + standardOutput = manFile.outputStream() + } + } + } + +val shadowJar = tasks.named("shadowJar", Jar::class.java) +tasks.register("generateManPage", GenerateManPage::class.java) { + notCompatibleWithConfigurationCache("Exec-based stream redirection and script-defined task class can hold non-serializable references.") + dependsOn(shadowJar) + shadowJarFile.set(shadowJar.flatMap { it.archiveFile }) +} + +abstract class InstallManPage + @Inject + constructor() : + DefaultTask() { + @get:Inject + protected abstract val layout: ProjectLayout + + @get:Inject + protected abstract val providers: ProviderFactory + + @get:InputFile + abstract val sourceFileProperty: RegularFileProperty + + @get:Input + abstract val manInstallDirectory: Property + + @get:OutputFile + abstract val outputGzipFile: RegularFileProperty + + init { + description = "Installs the man page cag.1.gz to the man1 directory." + group = "documentation" + + manInstallDirectory.convention(providers.gradleProperty("manInstallDir").orElse(defaultDirectory())) + + outputGzipFile.convention( + manInstallDirectory.flatMap { dir -> + layout.file(providers.provider { File(dir).resolve("cag.1.gz") }) + } + ) + } + + private fun defaultDirectory(): String { + val osName = System.getProperty("os.name").lowercase() + val homePath = System.getProperty("user.home") + val defaultDirectory = + if (osName.contains("mac") || osName.contains("darwin")) { + val brewPrefix = brewPrefix() + if (brewPrefix != null) { + Paths.get(brewPrefix, "share", "man", "man1").toString() + } else { + Paths.get(homePath, "Library", "Man", "man1").toString() + } + } else { + Paths.get(homePath, ".local", "share", "man", "man1").toString() + } + return defaultDirectory + } + + @TaskAction + fun install() { + val sourceFile = sourceFileProperty.get().asFile + require(sourceFile.exists()) { "Man page not found at $sourceFile. Run :cli:generateManPage first." } + + val chosenDirectory = manInstallDirectory.get() + val homePath = System.getProperty("user.home") + val resolvedDirectory = + if (chosenDirectory.startsWith("~/")) { + File(Paths.get(homePath, chosenDirectory.removePrefix("~/")).toString()) + } else { + File(chosenDirectory) + } + val outputFile = File(resolvedDirectory, "cag.1.gz") + val outputDirectory = outputFile.parentFile + + val osName = System.getProperty("os.name").lowercase() + if ((osName.contains("mac") || osName.contains("darwin")) && outputDirectory.absolutePath.startsWith("/usr/local/")) { + logger.warn( + "Target is under /usr/local; you may need elevated permissions (sudo) or " + + "choose a user directory like ~/Library/Man/man1" + ) + } + + if (!outputDirectory.exists()) { + check(outputDirectory.mkdirs()) { "Failed to create man directory: $outputDirectory" } + } + + FileInputStream(sourceFile).use { fileInputStream -> + FileOutputStream(outputFile).use { fileOutputStream -> + GZIPOutputStream(fileOutputStream).use { gzipOutputStream -> + fileInputStream.copyTo(gzipOutputStream) + } + } + } + + println("Installed man page: $outputFile") + if (osName.contains("linux")) { + println("You may need to update the man database (e.g., 'sudo mandb') on some systems.") + } + + val sectionPattern = Regex("man[1-9]") + val manRootDirectory = + if (sectionPattern.matches(outputDirectory.name)) { + outputDirectory.parentFile ?: outputDirectory + } else { + outputDirectory + } + println("Preview with: man -M \"${manRootDirectory.absolutePath}\" cag") + + val isMac = osName.let { it.contains("mac") || it.contains("darwin") } + if (isMac) { + val suggestedRoot = Paths.get(homePath, "Library", "Man").toString() + println("To make it permanent on zsh, add this line to ~/.zshrc then 'source ~/.zshrc':") + println(" export MANPATH=\"$suggestedRoot:$(manpath 2>/dev/null)\"") + val brewPrefix = brewPrefix() + if (brewPrefix != null) { + val brewMan1Path = Paths.get(brewPrefix, "share", "man", "man1").toString() + println("Alternatively, install system-wide (may require sudo):") + println(" ./gradlew :cli:installManPage -PmanInstallDir=$brewMan1Path") + } + } + } + + private fun brewPrefix(): String? { + val brewEnv = System.getenv("HOMEBREW_PREFIX") + val prefix = + when { + !brewEnv.isNullOrBlank() -> brewEnv + File("/opt/homebrew").exists() -> "/opt/homebrew" + File("/usr/local").exists() -> "/usr/local" + else -> null + } + return prefix + } + } + +tasks.register("installManPage", InstallManPage::class.java) { + dependsOn("generateManPage") + sourceFileProperty.set(layout.buildDirectory.file("man/cag.1")) +} diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt index 00da664..55d6d73 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/AppArgumentProcessor.kt @@ -30,6 +30,17 @@ private val PRIMARY_FLAGS = class AppArgumentProcessor(private val argumentParser: ArgumentParser = ArgumentParser()) { fun isHelpRequested(arguments: Array): Boolean = argumentParser.parsePrimaryWithSecondaries(arguments, HelpPrimary).isNotEmpty() + fun getHelpOptions(arguments: Array): HelpOptions? { + val primaryFlagMatches = argumentParser.parsePrimaryWithSecondaries(arguments, HelpPrimary) + if (primaryFlagMatches.isEmpty()) { + return null + } + val secondaryFlags = primaryFlagMatches.first() + val topic = secondaryFlags[SecondaryFlagConstants.HELP_TOPIC] + val format = secondaryFlags[SecondaryFlagConstants.HELP_FORMAT] + return HelpOptions(topic = topic, format = format) + } + private fun getAllPrimaryFlagStrings(): Set = PRIMARY_FLAGS.flatMap { listOf(it.long, it.short) }.toSet() fun validateNoUnknownFlags(arguments: Array) { @@ -233,4 +244,6 @@ class AppArgumentProcessor(private val argumentParser: ArgumentParser = Argument arguments[firstPrimaryIndex] == primaryFlag.long } } + + data class HelpOptions(val topic: String?, val format: String?) } diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt new file mode 100644 index 0000000..193da95 --- /dev/null +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/HelpContent.kt @@ -0,0 +1,181 @@ +package com.mitteloupe.cag.cli + +object HelpContent { + const val USAGE_SYNTAX: String = + "usage: cag " + + "[--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit]]... " + + "[--new-architecture [--no-compose] [--ktlint] [--detekt]]... " + + "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt]]... " + + "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... " + + "[--new-use-case --name=UseCaseName [--path=TargetPath]]... [--new-view-model --name=ViewModelName [--path=TargetPath]]..." + + fun helpSections(): Map = + buildMap { + put( + "new-project", + """ + Options: new-project + --new-project | -np + Generate a complete Clean Architecture project template + --name=ProjectName | -n=ProjectName | -n ProjectName | -nProjectName + Specify the project name (required) + --package=PackageName | --package PackageName | -p=PackageName | -p PackageName | -pPackageName + Specify the package name (required) + --no-compose | -nc + Disable Compose support for the project + --ktlint | -kl + Enable ktlint for the project + --detekt | -d + Enable detekt for the project + --ktor | -kt + Enable Ktor for data sources + --retrofit | -rt + Enable Retrofit for data sources + + Examples: + cag --new-project --name=MyApp --package=com.example.myapp + cag --new-project --name=MyApp --package=com.example.myapp --no-compose --ktlint --detekt + cag --new-project --name=MyApp --package=com.example.myapp --ktor --retrofit + """.trimIndent() + ) + put( + "new-architecture", + """ + Options: new-architecture + --new-architecture | -na + Generate a new Clean Architecture package with domain, presentation, and UI layers + --no-compose | -nc + Disable Compose support for the preceding architecture package + --ktlint | -kl + Enable ktlint for the preceding architecture package + --detekt | -d + Enable detekt for the preceding architecture package + + Examples: + cag --new-architecture + cag --new-architecture --no-compose + cag --new-architecture --ktlint --detekt + """.trimIndent() + ) + put( + "new-feature", + """ + Options: new-feature + --new-feature | -nf + Generate a new feature + --name=FeatureName | -n=FeatureName | -n FeatureName | -nFeatureName + Specify the feature name (required) + --package=PackageName | --package PackageName | -p=PackageName | -p PackageName | -pPackageName + Override the feature package for the preceding feature + --ktlint | -kl + Enable ktlint for the preceding feature (adds plugin and .editorconfig if missing) + --detekt | -d + Enable detekt for the preceding feature (adds plugin and detekt.yml if missing) + + Examples: + cag --new-feature --name=Profile + cag --new-feature --name=Profile --package=com.example.feature.profile + cag --new-feature --name=Profile --ktlint --detekt + """.trimIndent() + ) + put( + "new-datasource", + """ + Options: new-datasource + --new-datasource | -nds + Generate a new data source + --name=DataSourceName | -n=DataSourceName | -n DataSourceName | -nDataSourceName + Specify the data source name (required, DataSource suffix will be added automatically) + --with=ktor|retrofit|ktor,retrofit | -w=ktor|retrofit|ktor,retrofit + Attach dependencies to the preceding new data source + + Examples: + cag --new-datasource --name=User + cag --new-datasource --name=User --with=retrofit + cag --new-datasource --name=User --with=ktor,retrofit + """.trimIndent() + ) + put( + "new-use-case", + """ + Options: new-use-case + --new-use-case | -nuc + Generate a new use case + --name=UseCaseName | -n=UseCaseName | -n UseCaseName | -nUseCaseName + Specify the use case name (required) + --path=TargetPath | --path TargetPath | -p=TargetPath | -p TargetPath | -pTargetPath + Specify the target directory for the preceding use case + + Examples: + cag --new-use-case --name=FetchUser + cag --new-use-case --name=FetchUser --path=architecture/domain/src/main/kotlin + """.trimIndent() + ) + put( + "new-view-model", + """ + Options: new-view-model + --new-view-model | -nvm + Generate a new ViewModel + --name=ViewModelName | -n=ViewModelName | -n ViewModelName | -nViewModelName + Specify the ViewModel name (required) + --path=TargetPath | --path TargetPath | -p=TargetPath | -p TargetPath | -pTargetPath + Specify the target directory for the preceding ViewModel + + Examples: + cag --new-view-model --name=Profile + cag --new-view-model --name=Profile --path=architecture/presentation/src/main/kotlin + """.trimIndent() + ) + put( + "general", + """ + General + --help | -h [--topic=] [--format=man] + Show help. When a topic is provided, prints only that section. With --format=man outputs a roff man page to stdout. + Topics: new-project, new-architecture, new-feature, new-datasource, new-use-case, new-view-model, configuration + + Examples: + cag --help + cag --help --topic=new-feature + cag --help --format=man | col -b + """.trimIndent() + ) + put( + "configuration", + """ + 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 + """.trimIndent() + ) + } +} diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt index 1f079d5..c3403b0 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/Main.kt @@ -1,5 +1,6 @@ package com.mitteloupe.cag.cli +import com.mitteloupe.cag.cli.HelpContent.USAGE_SYNTAX import com.mitteloupe.cag.cli.configuration.ClientConfigurationLoader import com.mitteloupe.cag.cli.filesystem.CliFileSystemBridge import com.mitteloupe.cag.core.DirectoryFinder @@ -37,13 +38,6 @@ import java.nio.file.Paths import java.util.UUID import kotlin.system.exitProcess -private const val USAGE_SYNTAX = - "usage: cag [--new-project --name=ProjectName --package=PackageName [--no-compose] [--ktlint] [--detekt] [--ktor] [--retrofit]]... " + - "[--new-architecture [--no-compose] [--ktlint] [--detekt]]... " + - "[--new-feature --name=FeatureName [--package=PackageName] [--ktlint] [--detekt]]... " + - "[--new-datasource --name=DataSourceName [--with=ktor|retrofit|ktor,retrofit]]... " + - "[--new-use-case --name=UseCaseName [--path=TargetPath]]... [--new-view-model --name=ViewModelName [--path=TargetPath]]..." - fun main(arguments: Array) { val argumentProcessor = AppArgumentProcessor() val projectRoot = findGradleProjectRoot(Paths.get("").toAbsolutePath().toFile()) ?: Paths.get("").toAbsolutePath().toFile() @@ -52,7 +46,12 @@ fun main(arguments: Array) { val configuration = ClientConfigurationLoader().load(projectRoot) if (argumentProcessor.isHelpRequested(arguments)) { - printHelpMessage() + val helpOptions = argumentProcessor.getHelpOptions(arguments) + when { + helpOptions?.format?.lowercase() == "man" -> ManPagePrinter.printManPage(helpOptions.topic) + helpOptions?.topic != null -> printHelpMessage(helpOptions.topic) + else -> printHelpMessage() + } return } @@ -127,8 +126,7 @@ fun main(arguments: Array) { } featureRequests.forEach { requestFeature -> - val packageName = - requestFeature.packageName ?: basePackage?.let { "${'$'}it${'$'}{requestFeature.featureName.lowercase()}" } + val packageName = requestFeature.packageName ?: basePackage?.let { "$it${requestFeature.featureName.lowercase()}" } val request = GenerateFeatureRequestBuilder( @@ -286,6 +284,22 @@ private fun printHelpMessage() { ) } +private fun printHelpMessage(topic: String?) { + val normalized = topic?.lowercase()?.trim() + if (normalized.isNullOrEmpty() || normalized == "all" || normalized == "overview") { + printHelpMessage() + return + } + val sections = HelpContent.helpSections() + val content = sections[normalized] + if (content != null) { + println(content) + } else { + println("Unknown help topic: $topic\nAvailable topics: ${sections.keys.sorted().joinToString(", ")}\n") + printHelpMessage() + } +} + private fun produceGenerator(): Generator { val fileCreator = FileCreator(CliFileSystemBridge()) val directoryFinder = DirectoryFinder() diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/ManPagePrinter.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/ManPagePrinter.kt new file mode 100644 index 0000000..776baf4 --- /dev/null +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/ManPagePrinter.kt @@ -0,0 +1,41 @@ +package com.mitteloupe.cag.cli + +object ManPagePrinter { + fun printManPage(topic: String?) { + val name = "cag" + val section = 1 + val title = "Clean Architecture Generator" + val normalized = topic?.lowercase()?.trim() + + val sections = HelpContent.helpSections() + val topicsToRender = + if (normalized.isNullOrEmpty() || normalized == "all" || normalized == "overview") { + sections + } else { + sections.filterKeys { it == normalized } + } + + val content = + buildString { + appendLine(".TH ${name.uppercase()} $section \"\" \"\" \"cag\"") + appendLine(".SH NAME") + appendLine("$name - $title") + appendLine(".SH SYNOPSIS") + appendLine(".PP") + appendLine(HelpContent.USAGE_SYNTAX) + appendLine(".SH DESCRIPTION") + appendLine("cag generates Android Clean Architecture scaffolding and components.") + topicsToRender.forEach { (key, value) -> + appendLine(".SH ${key.uppercase()}") + value.lines().forEach { line -> + if (line.isBlank()) { + appendLine(".PP") + } else { + appendLine(line) + } + } + } + } + println(content) + } +} diff --git a/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt b/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt index 0393fdd..cbf3f5d 100644 --- a/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt +++ b/cli/src/main/kotlin/com/mitteloupe/cag/cli/flag/PrimaryFlag.kt @@ -12,6 +12,8 @@ object SecondaryFlagConstants { const val PATH = "--path" const val INPUT_TYPE = "--input-type" const val OUTPUT_TYPE = "--output-type" + const val HELP_TOPIC = "--topic" + const val HELP_FORMAT = "--format" } interface PrimaryFlag { @@ -117,6 +119,10 @@ interface PrimaryFlag { data object HelpPrimary : PrimaryFlag { override val long = "--help" override val short = "-h" - override val secondaryFlags: List = emptyList() + override val secondaryFlags: List = + listOf( + SecondaryFlag(SecondaryFlagConstants.HELP_TOPIC, "-t"), + SecondaryFlag(SecondaryFlagConstants.HELP_FORMAT, "-f") + ) } }