thunderbird / thunderbird/thunderbird-android

Add an architecture-aware CLI for scaffolding Gradle modules

Open
#11,324 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: help wanted type: developer experience
Dominant language
Kotlin
Stars
14k
Forks
2.8k
Avg merge
3d 3h
Merged PRs (30d)
57

Description

Summary

Creating a Gradle module currently requires manually creating its structure, selecting the correct build conventions, and registering it in multiple locations. This is repetitive and can result in inconsistent module structures, invalid names, or missed registrations.

Add a Kotlin/JVM CLI that scaffolds new modules and enforces the repository’s documented architecture and naming conventions. It should support interactive and non-interactive execution.

This is a follow-up to this PR review comment.

CLI implementation

The module generator must follow the project’s existing CLI conventions:

  • Implement it as a Kotlin/JVM module under :cli:module-generator-cli.
  • Apply ThunderbirdPlugins.App.jvm.
  • Use Clikt through libs.clikt.
  • Use net.thunderbird.cli.module.generator as its package.
  • Provide a Main.kt entry point delegating to a CliktCommand.
  • Configure the main class as net.thunderbird.cli.module.generator.MainKt.
  • Set version = "unspecified".
  • Include the module in the root buildCliTools task.
  • Add a thin wrapper at scripts/module-generator that builds the distribution and forwards all arguments to the CLI.
  • Keep all generation, validation, and file-update logic in Kotlin.
  • Add a README under cli/module-generator-cli/ and list the wrapper in scripts/README.md.

Example:

./scripts/module-generator

CLI arguments

Option Clikt/Kotlin type Value count Required Applies to Description
--type <type> ModuleType? using enum<ModuleType>() 1 Required in non-interactive mode All module types Module type: feature, core, library, legacy, or cli.
--name <name> String? 1 Required in non-interactive mode All module types Colon-separated module area and optional subareas without the type prefix or generated suffixes.
--internal-only Boolean using flag() 0 No feature, core Generate only the standard internal module for an existing API module.
--internal-variant <variant>... List<String> using varargValues(min = 1) 1..N when present No feature, core Generate one or more qualified internal variants for an existing API module.
--yes Boolean using flag() 0 No All module types Skip the final confirmation.
--allow-legacy Boolean using flag() 0 Required for non-interactive legacy creation legacy Acknowledge that the module is intended to isolate or migrate existing legacy code.
--non-interactive Boolean using flag() 0 No All module types Disable prompts and fail when required input is missing.
--help Clikt eager option 0 No All module types Display usage information and exit.

Clikt supports an option with a variable number of whitespace-separated values through varargValues. By default, it requires at least one value and has no maximum.

The intended declaration is equivalent to:

private val internalVariants: List<String> by option(
    "--internal-variant",
    help = "Internal implementation variants to generate.",
).varargValues(min = 1)

This allows one variant:

--internal-variant eml

or multiple variants in the same option occurrence:

--internal-variant eml pdf markdown

Clikt’s multiple() handles repeating the entire option, such as --internal-variant eml --internal-variant pdf. That is not required for this CLI because varargValues(min = 1) directly represents the desired 1..N value list.

--internal-only and --internal-variant represent different layouts and must not be used together.

Interactive input

When required options are not provided, the CLI should ask for the module type:

  • feature
  • core
  • library
  • legacy
  • cli

It should then ask for the module name as a colon-separated path without its type prefix, for example:

  • newmodule
  • mail:newfeature
  • mail:message:export
Feature and core layout

For feature and core, ask which layout to create:

  1. api and internal
  2. internal only
  3. One or more qualified internal variants only

Creating both api and internal should remain the default.

When qualified variants are selected interactively, the CLI should collect one or more variant names and apply the same validation as --internal-variant.

Examples:

Type Layout Input Generated Gradle projects
Feature API and internal newmodule :feature:newmodule:api, :feature:newmodule:internal
Feature API and internal mail:newfeature :feature:mail:newfeature:api, :feature:mail:newfeature:internal
Feature Internal only mail:newfeature :feature:mail:newfeature:internal
Feature Internal variants mail:message:export + eml :feature:mail:message:export:internal-eml
Feature Internal variants mail:message:export + eml, pdf :feature:mail:message:export:internal-eml, :feature:mail:message:export:internal-pdf
Core API and internal network:client :core:network:client:api, :core:network:client:internal
Core Internal only network:client :core:network:client:internal
Core Internal variants storage + sqlite, inmemory :core:storage:internal-sqlite, :core:storage:internal-inmemory
Library Not applicable newmodule :library:newmodule
Legacy Not applicable newmodule :legacy:newmodule
CLI Not applicable newtool :cli:newtool-cli

When creating only internal modules:

  • The corresponding api module must already exist and be registered in settings.gradle.kts.
  • Every generated internal module must depend on the existing API module.
  • The CLI must not modify or regenerate the API module.
  • The operation must fail before writing files if the API module cannot be found.
  • The operation must fail if any requested internal module already exists.
  • Creating multiple variants must be atomic: either every requested variant is created or none are.

For CLI modules, add the -cli suffix automatically:

Gradle project: :cli:newtool-cli
Command name:   newtool
Wrapper:        scripts/newtool

Non-interactive execution

Options should take precedence over interactive input. If only some required values are provided, the CLI may prompt for missing values unless --non-interactive is used.

Create an API/internal pair:

./scripts/module-generator \
    --type feature \
    --name mail:newfeature \
    --yes

Create only the standard internal module:

./scripts/module-generator \
    --type feature \
    --name mail:newfeature \
    --internal-only \
    --yes

Create one qualified internal variant:

./scripts/module-generator \
    --type feature \
    --name mail:message:export \
    --internal-variant eml \
    --yes

Create multiple qualified internal variants:

./scripts/module-generator \
    --type feature \
    --name mail:message:export \
    --internal-variant eml pdf markdown \
    --yes

This should atomically create:

:feature:mail:message:export:internal-eml
:feature:mail:message:export:internal-pdf
:feature:mail:message:export:internal-markdown

with package roots:

net.thunderbird.feature.mail.message.export.internal.eml
net.thunderbird.feature.mail.message.export.internal.pdf
net.thunderbird.feature.mail.message.export.internal.markdown

When --non-interactive is provided:

  • Missing required options must produce a clear error.
  • The CLI must not prompt.
  • Validation failures must return a non-zero exit code.
  • Legacy creation must require --allow-legacy.
  • Interactive and non-interactive execution must produce identical files for the same inputs.

Internal-only options must be rejected for library, legacy, and cli.

Module naming conventions

The CLI must validate names using:

Enforce the following rules:

  • The supplied name represents only the module area and optional subareas.
  • The module type prefix is derived from --type.
  • Module path segments must be lowercase.
  • Multi-word Gradle module segments should use hyphens.
  • Module names must start with a letter.
  • Module names may contain only lowercase ASCII letters, digits, and hyphens.
  • Colon-separated paths must not contain empty segments.
  • CLI project names must end in -cli; the generator adds this suffix automatically.
  • Generated package names must use valid lowercase Kotlin package identifiers.
  • Hyphens in Gradle names must not appear in package names.
  • Shared module packages must use net.thunderbird.
  • New feature and core implementations must use internal, never impl.
  • The supplied name must not contain generated suffixes such as api, internal, or -cli.
  • Internal variant qualifiers must be lowercase and alphanumeric.
  • Variant qualifiers must be unique within a single invocation.
  • Reserved type names must not be accepted when they would create an ambiguous path.

Feature modules must use:

:feature:<area>[:<subarea>]:api
:feature:<area>[:<subarea>]:internal
:feature:<area>[:<subarea>]:internal-<variant>

Core modules must use:

:core:<area>[:<subarea>]:api
:core:<area>[:<subarea>]:internal
:core:<area>[:<subarea>]:internal-<variant>

Package names must follow the documented mapping:

Gradle project Package root
:feature:account:settings:api net.thunderbird.feature.account.settings
:feature:account:settings:internal net.thunderbird.feature.account.settings.internal
:feature:mail:message:export:internal-eml net.thunderbird.feature.mail.message.export.internal.eml
:core:network:api net.thunderbird.core.network
:core:network:internal net.thunderbird.core.network.internal
:core:storage:internal-sqlite net.thunderbird.core.storage.internal.sqlite
:cli:resource-mover-cli net.thunderbird.cli.resource.mover

API packages must not contain an .api segment.

Validate every input before creating or modifying files. Rejected values should produce an actionable error containing the violated convention and a valid example.

Module generation

The CLI should:

  • Create module directories and build.gradle.kts files.
  • Use the repository’s convention plugins and current templates.
  • Generate valid namespaces and package paths.
  • Produce modules that build without additional configuration.
  • Avoid adding speculative dependencies or application wiring.

For feature and core API/internal pairs:

  • Generate both projects.
  • Make internal depend on its sibling api.
  • Keep API packages free of an .api segment.
  • Place implementation packages below .internal.

For standard internal-only generation:

  • Verify the sibling API project and directory exist.
  • Verify the API project is registered in settings.gradle.kts.
  • Generate only the standard internal project.
  • Add a dependency on the existing API project.
  • Leave the API project unchanged.

For qualified internal variants:

  • Accept a List<String> containing one or more variants.
  • Generate one internal-<variant> project per value.
  • Add a dependency from each variant to the existing API project.
  • Map each module to .internal.<variant>.
  • Validate all variants and targets before writing files.
  • Reject duplicate variants rather than silently deduplicating them.
  • Treat generation and registration as one atomic operation.
  • Leave the API project unchanged.

For CLI modules:

  • Apply ThunderbirdPlugins.App.jvm.
  • Add libs.clikt.
  • Generate a CliktCommand.
  • Generate a Main.kt entry point.
  • Configure mainClass.
  • Use net.thunderbird.cli.<name>.
  • Add the -cli project suffix.
  • Add a thin wrapper under scripts/.
  • Add a module README.
  • Ensure the module is included in buildCliTools.

Gradle registration

The CLI should:

  • Register every generated project in settings.gradle.kts.
  • Add entries to the appropriate include(...) group.
  • Preserve formatting and organization.
  • Avoid duplicate registrations.
  • Register multiple internal variants together.
  • Update additional type-specific configuration when required.

For CLI modules, ensure the project is included in buildCliTools. If that task already discovers all :cli:* projects, verify the generated path matches that convention without modifying the root build unnecessarily.

Architecture enforcement

The generated structure must follow:

Because legacy modules must not contain new development, selecting legacy should display a warning and require explicit confirmation that the module is intended to isolate or migrate existing legacy code.

The CLI should reject:

  • Empty names or path segments.
  • Uppercase characters.
  • Underscores, whitespace, or unsupported characters.
  • Segments beginning or ending with a hyphen.
  • Consecutive hyphens.
  • Malformed Gradle paths.
  • Names that include a type prefix.
  • Names containing generated api, internal, -cli, or deprecated impl suffixes.
  • An empty --internal-variant.
  • Invalid or duplicate internal variants.
  • Internal-only generation when the API module does not exist.
  • Internal-only options used with unsupported module types.
  • Conflicting --internal-only and --internal-variant options.
  • Modules, package directories, or wrappers that already exist.
  • Inputs that violate the documented module hierarchy.

Validation failures must not leave partial files or registrations behind.

User experience

Before writing files, interactive execution should display:

  • The selected module type.
  • The selected feature/core layout.
  • The normalized module name.
  • All requested internal variants.
  • The Gradle projects that will be created.
  • The existing API project used for internal-only generation.
  • Generated namespaces and package roots.
  • Directories and build files that will be generated.
  • Files that will be updated.

Ask the user to confirm unless --yes was provided.

Provide actionable errors and return a non-zero exit code when generation fails.

Documentation

Document:

  • Interactive usage.
  • Non-interactive usage.
  • The CLI argument table.
  • Clikt’s varargValues(min = 1) behaviour.
  • Supported module types and layouts.
  • API/internal pair generation.
  • Standard internal-only generation.
  • Single and multiple internal variant generation.
  • Valid and invalid naming examples.
  • Generated project and package mappings.
  • Architecture restrictions.
  • Failure and recovery behaviour.

The primary documentation should live at:

cli/module-generator-cli/README.md

The wrapper should also be listed in:

scripts/README.md

Testing

Add automated Kotlin tests covering:

  • Every supported module type.
  • Every feature/core layout.
  • Interactive and non-interactive execution.
  • API/internal pair generation.
  • Standard internal-only generation.
  • --internal-variant with one value.
  • --internal-variant with multiple whitespace-separated values.
  • An empty --internal-variant.
  • Duplicate and invalid variants.
  • Atomic failure when any requested variant cannot be created.
  • Detection of an existing sibling API module.
  • Failure when the sibling API is missing or unregistered.
  • Variant name validation and package mapping.
  • CLI module, command, and wrapper generation.
  • Gradle module naming validation.
  • Namespace and package derivation.
  • Rejection of deprecated impl naming.
  • Rejection of conflicting option combinations.
  • Registration in settings.gradle.kts.
  • CLI discovery by buildCliTools.
  • Existing-module conflicts.
  • Legacy acknowledgement requirements.
  • Failure without partial modifications.
  • Re-running without duplicate registrations.

Use fakes for filesystem access so generation and failure behaviour can be tested without modifying the real repository.

Acceptance criteria

  • The generator is implemented as :cli:module-generator-cli.
  • It is written in Kotlin and uses Clikt.
  • It follows the existing JVM CLI module structure.
  • A thin scripts/module-generator wrapper builds and invokes it.
  • The CLI supports interactive and fully non-interactive execution.
  • Its options and Kotlin types are documented in a table.
  • It supports --type, --name, --internal-only, --internal-variant, --yes, --allow-legacy, --non-interactive, and --help.
  • --internal-variant uses Clikt’s varargValues(min = 1).
  • --internal-variant accepts one or more whitespace-separated values.
  • Multiple variants are validated and generated atomically.
  • Duplicate variants are rejected before modifying files.
  • Feature and core generation defaults to an API/internal pair.
  • It can generate only the standard internal module for an existing API.
  • It can generate one or more qualified internal variants for an existing API.
  • Internal-only generation verifies that the sibling API exists and is registered.
  • Generated internal modules depend on their sibling API.
  • Qualified variants use internal-<variant> and .internal.<variant>.
  • Internal-only options are rejected for unsupported module types.
  • It validates module names before modifying files.
  • It enforces documented module, namespace, and package conventions.
  • It uses internal rather than the deprecated impl.
  • Generated projects are registered in settings.gradle.kts.
  • Generated CLI modules use -cli and are included in buildCliTools.
  • Invalid input cannot produce partial changes.
  • Legacy module creation requires explicit acknowledgement.
  • Automated Kotlin tests cover generation, naming, validation, and registration.
  • Usage documentation is included.
  • Generated modules are recognized by Gradle and build successfully.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading docs/architecture/module-structure.md and the two linked ADRs, then inspect the root buildCliTools task and existing CLI conventions. The implementation belongs under cli/module-generator-cli with Main.kt, plus scripts/module-generator, cli/module-generator-cli/README.md, and scripts/README.md. Done means validated interactive and non-interactive scaffolding updates the required Gradle registrations and files atomically, with the specified options and errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin
Domain
build-system, cli, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.