Architecture & Internals

Quarkus Forge follows a layered architecture that separates HTTP transport, JSON parsing, domain logic, UI state management, and rendering into focused modules.

High-Level Architecture

graph TD
    subgraph Entry["CLI Layer (cli/)"]
        CLI[QuarkusForgeCli]
        HCLI[HeadlessCli]
    end

    subgraph Runtime["Runtime Layer (runtime/)"]
        RS[RuntimeServices]
        TBS[TuiBootstrapService]
    end

    subgraph Persistence["Persistence Layer (persistence/)"]
        UPS[UserPreferencesStore]
        EFS[ExtensionFavoritesStore]
    end

    subgraph TUI["UI Layer (ui/)"]
        CTC[CoreTuiController]
    end

    subgraph Headless["Headless Layer (headless/)"]
        HGS[HeadlessGenerationService]
        HERS[HeadlessExtensionResolutionService]
        HFPS[HeadlessForgefilePersistenceService]
        HGI[HeadlessGenerationInputs]
        HGES[HeadlessGenerationExecutionService]
        HCC[HeadlessCatalogClient]
        HOP[HeadlessOutputPrinter]
        HT[HeadlessTimeouts]
    end

    subgraph App["Application Layer (application/)"]
        IRS[InputResolutionService]
        SSS[StartupStateService]
        DSS[DefaultStartupStateService]
        SML[LiveStartupMetadataLoader]
    end

    subgraph Forge["Forge Layer (forge/)"]
        FFS[ForgefileStore]
    end

    subgraph API["API Layer (api/)"]
        QAC[QuarkusApiClient]
        APP[ApiPayloadParser]
        JFR[JsonFieldReader]
        CSC[CatalogSnapshotCache]
        CDS[CatalogDataService]
    end

    subgraph Archive["Archive Layer (archive/)"]
        PAS[ProjectArchiveService]
        SZE[SafeZipExtractor]
    end

    subgraph PostGen["Post-Generation Layer (postgen/)"]
        PTA[PostTuiActionExecutor]
        SE[ShellExecutor]
        ID[IdeDetector]
    end

    CLI --> SSS
    CLI --> RS
    CLI --> TBS
    HCLI --> RS
    HCLI --> HGS
    HGS --> HERS
    HGS --> HFPS
    HGS --> HGI
    HGS --> HGES
    HGS --> HCC
    HGS --> HOP
    HGS --> HT
    HGS --> IRS
    HERS --> HCC
    HERS --> EFS
    TBS --> CTC
    TBS --> RS
    CTC --> QAC
    CTC --> FFS
    HCC --> QAC
    QAC --> APP
    APP --> JFR
    RS --> QAC
    RS --> CDS
    RS --> PAS
    RS --> EFS
    QAC --> CDS
    CDS --> CSC
    QAC --> PAS
    PAS --> SZE
    PTA --> SE
    PTA --> ID

RuntimeServices now defines the runtime composition root for session-lifetime API/catalog/archive/favorites collaborators plus preference-store access used by CLI entrypoints. TuiBootstrapService, QuarkusForgeCli, and HeadlessCli now consume that explicit runtime surface directly instead of routing through a separate static wiring bag, and headless opening is owned by the runtime session itself rather than a free-floating static assembly path.

Within headless/, HeadlessGenerationService is now an orchestration shell: HeadlessGenerationInputs.fromCommand(…​) owns command/Forgefile reconciliation, HeadlessExtensionResolutionService owns extension and preset resolution, HeadlessForgefilePersistenceService owns lock/persistence rules, HeadlessGenerationExecutionService owns generation request execution and timeout cancellation, and HeadlessCatalogOperations is the narrow orchestration seam implemented by HeadlessCatalogClient.

Startup policy now lives under application/: LiveStartupMetadataLoader owns the live-metadata timeout and snapshot-fallback path, while DefaultStartupStateService merges explicit CLI overrides, optional stored prefill, and compiled defaults before handing the result to InputResolutionService.

UI Layer Design

The TUI layer uses an immediate-mode rendering architecture built on the TamboUI framework.

Component Decomposition

CoreTuiController (Orchestration Shell)
    │
    ├── UiEventRouter           Preserves key-priority contract
    │       │
    │       ├── Overlay handlers (command palette, help, post-gen menu)
    │       ├── Cancel flow (filter unwind priority chain)
    │       ├── Global shortcuts
    │       ├── Focus navigation
    │       └── Input handling
    │
    ├── UiRenderStateAssembler   Assembles immutable render models from reducer state and runtime-only render context
    │
    ├── CoreUiReducer            Pure transition logic for migrated slices
    │       ├── post-generation intent handling
    │       ├── command palette + help overlay visibility handling
    │       ├── submit + blocked-submit authority
    │       ├── generation callback intent handling
    │       ├── extension cancel + list interaction intent handling
    │       ├── focus/input intent handling
    │       ├── catalog load / reload lifecycle handling
    │       ├── startup overlay visibility handling
    │       └── error-details expansion/reset state
    │
    ├── UiEffectsRunner          Executes side effects emitted by reducer
    │       ├── catalog load / reload start
    │       ├── extension catalog interaction commands
    │       ├── catalog success application
    │       ├── generation start/cancel
    │       ├── generation-state transitions for submit lifecycle
    │       └── repaint + async coordination hooks
    │
    ├── UiRenderer               State-driven render orchestration
    │       ├── CoreUiRenderAdapter      Render adapter extracted from controller
    │       ├── CompactFieldRenderer     Metadata field widgets + validation hints
    │       ├── OverlayRenderer          Stateless overlay rendering
    │       ├── BodyPanelRenderer        Layout: metadata + extensions panels
    │       └── FooterLinesComposer      Status bar composition
    │
    ├── State Managers
    │       ├── GenerationState          Top-level generation phase model
    │       ├── MetadataSelectorManager  Platform/build/java selectors
    │       ├── ExtensionCatalogProjection  Search/filter state + row projection
    │       ├── ExtensionCatalogNavigation  Selection + list navigation
    │       ├── ExtensionCatalogPreferences Favorites/recents persistence
    │       ├── CatalogLoadState         Sealed type: Loading → Loaded | Failed
    │       └── GenerationStateTracker   Generation progress tracking
    │
    └── Extracted Helpers
            ├── PostGenerationMenuState    Post-gen key decoding + action menu configuration
            ├── ExtensionInteractionHandler  Bridge to catalog interactions
            ├── CatalogRowBuilder            Pure function: row building
            ├── UiTextConstants              UI text content
            ├── CatalogLoadCoordinator       Async catalog lifecycle + startup-overlay timing via reducer intents
            ├── GenerationFlowCoordinator    Async generation lifecycle
            ├── GenerationStateMachine       Allowed generation-state transitions
            └── UiTextInputKeys              Text-input edit-key normalization

Reducer-visible UI semantics are now converged for form and extension read-model state. ProjectRequest, ValidationReport, extension search/filter/read-model summaries, catalog lifecycle semantics, and overlay visibility flags update through reducer intents/effects and live in reducer-owned UiState. UiRenderModel is assembled immediately before render and carries the derived metadataPanel, extensionsPanel, footer, generation, and startupOverlay slices. Mutable widget internals such as TextInputState and ListState stay in adapter/runtime context rather than UiState.

UiState Ownership Map

Reducer-owned UiState fields:

  • request, validation, focusTarget

  • statusMessage, errorMessage, verboseErrorDetails, showErrorDetails

  • submitRequested, submitBlockedByValidation, submitBlockedByTargetConflict

  • commandPaletteSelection, overlays, catalogLoad, postGeneration, extensions

Render-only UiRenderModel fields:

  • metadataPanel, extensionsPanel, footer

  • generation, startupOverlay

The render-only fields above are synchronized from runtime collaborators immediately before render. They are not reducer authority, and UiRenderModel is the single container for those slices.

State Machine: Generation Flow

stateDiagram-v2
    [*] --> IDLE
    IDLE --> VALIDATING: Enter / Alt+G
    VALIDATING --> ERROR: validation fails
    VALIDATING --> IDLE: submit reset
    VALIDATING --> LOADING: validation passes
    LOADING --> CANCELLED: Esc / Ctrl+C
    LOADING --> ERROR: generation fails
    LOADING --> SUCCESS: download complete
    ERROR --> IDLE: dismissed
    CANCELLED --> IDLE: dismissed
    SUCCESS --> PostGenMenu: show menu
    PostGenMenu --> IDLE: Generate Again

    state PostGenMenu {
        [*] --> ActionList
        ActionList --> ExportForgefile
        ActionList --> PublishGitHub
        ActionList --> OpenIDE
        ActionList --> OpenTerminal
        ActionList --> Quit
        PublishGitHub --> VisibilitySubMenu
    }

State Machine: Catalog Loading

stateDiagram-v2
    [*] --> Initial: CatalogLoadState.initial()
    Initial --> Loading1: loadExtensionCatalogAsync()
    state Loading1 <>
    Loading1 --> Loaded_live: success
    Loading1 --> Failed: failure
    Loaded_live --> Loading2: reload requested
    state Loading2 <>
    Loading2 --> Loaded_new: success
    Loading2 --> Loaded_live: failure (preserves catalog)

    note right of Initial: Loaded("snapshot", false, false)
    note right of Loading1: Loading(previous=Initial)
    note right of Loading2: Loading(previous=Loaded_live)

UI State-Machine Refactor Invariants (ADR-UI-001)

The UI refactor to explicit UiState/UiIntent/UiReducer/UiEffect must preserve these behavior contracts:

  • Routing precedence remains fixed in UiEventRouter:

    1. help overlay keys / toggle

    2. command palette toggle + keys

    3. post-generation menu

    4. extension cancel flow

    5. quit flow

    6. loading lock handling

    7. global shortcuts

    8. focus navigation

    9. submit flow

    10. extension flow

    11. metadata selectors

    12. text input

  • Generation lifecycle semantics from GenerationFlowCoordinator remain unchanged:

    1. token-based stale completion protection

    2. cancellation semantics (CancellationException and cancel flag)

    3. dropped-callback reconciliation via reconcileCompletionIfDone

  • Post-generation flow semantics remain unchanged:

    1. action selection and visibility sub-menu behavior

    2. stable exit-plan generation

    3. Generate again returns to active TUI session without quitting

  • Non-key event behavior remains unchanged:

    1. TickEvent drives repaint/progress updates

    2. ResizeEvent updates status text while preserving state

    3. unknown events are ignored

  • Overlay rendering order remains fixed:

    1. header/body/footer

    2. generation overlay

    3. command palette

    4. help overlay

    5. post-generation menu

    6. startup overlay

  • State updates follow single-writer discipline: reducer transitions execute on the render thread.

  • TUI refactor cannot change headless behavior or CLI semantics.

Maintainer Checklists

Before merging architecture-sensitive changes, re-check the contracts below instead of relying on local intuition:

Routing And Overlay Changes

  • Preserve UiEventRouter precedence: help, command palette, post-generation menu, extension cancel, quit, loading lock, global shortcuts, focus navigation, submit, extension flow, metadata selectors, text input.

  • Keep Esc unwind order stable for extensions: search query, favorites-only, selected-only, preset filter, category filter, search-to-list focus shift, then quit.

  • Re-run the focused routing/state suites before changing key handling or overlay visibility behavior.

Reducer / Effects Changes

  • Keep reducer state pure and immutable; push imperative work through UiEffect handling.

  • Route follow-up semantic state changes back through UiIntent instead of mutating reducerState directly from callbacks or helper methods.

  • Keep mutable widget internals (TextInputState, ListState, terminal handles) outside UiState.

  • If a change touches form authority, keep ProjectRequest and ValidationReport synchronized through reducer-owned state.

Native Release Changes

  • Preserve CI native-size gating in .github/workflows/ci.yml for both -Pheadless,native and full -Pnative builds.

  • Update scripts/CheckNativeSize.java tests when native report parsing or summary output changes, especially for malformed reports and size-budget failures.

  • Re-run the documented serial verification commands before merging native/release workflow changes.

Extension Cancel Flow (Esc Priority Chain)

When the user presses Esc in the extension search or list, the TUI unwinds filters in priority order:

 Esc pressed in extension search/list
      │
      ├──[1] Search text not empty? ──► Clear search
      │
      ├──[2] Favorites filter on? ──► Disable favorites filter
      │
      ├──[3] Selected-only filter on? ──► Disable selected-only filter
      │
      ├──[4] Preset filter active? ──► Clear preset filter
      │
      ├──[5] Category filter active? ──► Clear category filter
      │
      ├──[6] In search field? ──► Focus extension list
      │
      └──[7] None of the above ──► Fall through to quit flow

API Layer Design

Architectural Guardrails

Headless/TUI boundaries are protected by ArchUnit tests to prevent dependency drift:

  • application/ stays independent from cli, headless, runtime, postgen, persistence, and ui.

  • Headless classes must not depend on dev.ayagmar.quarkusforge.runtime.*.

  • Headless orchestration classes must not depend on dev.ayagmar.quarkusforge.ui.*.

  • Headless orchestration classes must not depend on dev.tamboui.*.

  • Only the headless orchestration boundary may depend on CLI command/exit types.

  • CLI classes must not depend directly on dev.ayagmar.quarkusforge.persistence.*.

  • Only RuntimeServices may cross from runtime/ into headless/ to open the headless generation service.

  • UI classes must not depend on cli, headless, or runtime.

  • Post-generation classes must not depend on cli, headless, or persistence.

  • TuiSessionSummary lives in postgen/, so post-generation actions no longer depend back on runtime/.

  • Persistence classes must not depend on cli, headless, runtime, ui, or postgen.

  • Core non-UI layers (application, api, archive, diagnostics, domain, forge, headless, persistence, util) must not depend on ui.

Transport vs Parsing Separation

graph TD
    REQ[HTTP Request] --> QAC
    subgraph QAC["QuarkusApiClient (Transport)"]
        direction TB
        R[Retry with backoff]
        T[Timeout management]
        H[HTTP status handling]
        C[Cancellation support]
    end
    QAC -->|raw JSON| APP
    subgraph APP["ApiPayloadParser (Pure Parsing)"]
        direction TB
        E[Extension deserialization]
        M[Metadata extraction]
        S[Stream/preset parsing]
    end
    APP --> JFR
    subgraph JFR["JsonFieldReader (Shared)"]
        direction TB
        RS[readString / readInt]
        RL[readStringList]
        RO[readNestedObject]
    end

Catalog Freshness Strategy

graph TD
    START[Application Start] --> LM[Load metadata sync]
    LM -->|success| MCC[MetadataCompatibilityContext]
    LM -->|failure| BSF[Bundled snapshot fallback]
    MCC -->|constrains build/java| LE[Load extensions async]
    LE --> LIVE[Live API]
    LE --> CACHE[Cache fallback]
    LIVE --> CSC[CatalogSnapshotCache]
    CACHE --> CSC
    BSF --> RENDER[TUI renders immediately
with default extensions] CSC --> REPLACE[Catalog replaces defaults
when ready]

Domain Layer

graph LR
    subgraph Domain["Domain Layer (domain/)"]
        PR["ProjectRequest
(immutable)"] PRV["ProjectRequestValidator"] MCC["MetadataCompatibilityContext"] CPM["CliPrefillMapper"] end subgraph App["Application Layer (application/)"] IRS["InputResolutionService"] SSS["StartupStateService
DefaultStartupStateService"] SML["LiveStartupMetadataLoader
StartupRequest
StartupState"] end subgraph Forge["Forge Layer (forge/)"] FF["Forgefile + ForgefileLock"] FFS["ForgefileStore"] end IRS -->|resolves| PR IRS -->|uses| CPM IRS -->|uses| MCC SSS -->|owns startup policy for| IRS SML -->|loads metadata for| SSS PRV -->|validates| PR MCC -->|constrains| PR CPM -->|maps CLI options| PR FFS -->|read/write| FF

Archive Layer

graph TD
    ZIP[ZIP Download] --> PAS[ProjectArchiveService]
    PAS -->|progress reporting| SZE[SafeZipExtractor]
    PAS -->|cancellation check| SZE
    SZE --> ZS["Zip-Slip protection
(path traversal guard)"] SZE --> ZB["Zip-Bomb protection
(size + ratio + entry count)"] SZE --> ROOT["Root directory normalization
(strips single root folder)"]

Design Principles

Principle Application

Single Responsibility

Each class has one reason to change. QuarkusApiClient handles HTTP; ApiPayloadParser handles JSON; OverlayRenderer handles overlay rendering. QuarkusForgeCli owns CLI entry; TuiBootstrapService owns runtime bootstrap; HeadlessCli owns the headless-only entry path.

Sealed Types

CatalogLoadState (Loading/Loaded/Failed) and MenuKeyResult (Handled/Quit/ExportRecipe/GenerateAgain) make state transitions explicit and exhaustive.

Immutable Snapshots

MetadataPanelSnapshot, ExtensionsPanelSnapshot, FooterSnapshot are immutable records passed to renderers — no side effects.

DRY

JsonFieldReader deduplicates JSON helpers across 5+ classes. renderCompactField consolidates two near-identical rendering methods. BoundaryFailure centralizes boundary failure classification. applyFavoritesAndPresetFilters eliminates duplicate filter pipeline.

Derived State Elimination

successHint is computed from reducer-owned UiState.PostGenerationView rather than stored as a separate controller field.

Single Source of Truth

Reducer-visible TUI semantics are synchronized onto reducerState before reduce/render; UiRenderStateAssembler overlays render-only panel/footer snapshots while keeping reducer-owned slices intact.

Encapsulation

CatalogLoadCoordinator and MetadataSelectorManager own their state; PostGenerationMenuState only translates keys against reducer-owned post-generation data.

Resource Safety

QuarkusApiClient implements AutoCloseable; all instantiation sites use try-with-resources or whenComplete callbacks to prevent thread pool leaks.

Atomic Persistence

persistence.FileBackedExtensionFavoritesStore.saveAll() writes both favorites and recents in a single file operation, preventing data loss from split read-modify-write.

Testing Strategy

  • Extensive unit tests with deterministic async harnesses for TUI state machine testing

  • Integration tests using WireMock for API contract verification

  • Dedicated unit tests for extracted collaborators: PostGenerationMenuState, MetadataSelectorManager, CatalogRowBuilder, StartupOverlayTracker

  • Async TUI tests use UiScheduler.immediate() and Duration.ZERO debounce for deterministic behavior

  • Property-based tests for ZIP extraction edge cases

See Testing Strategy for details.