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:-
help overlay keys / toggle
-
command palette toggle + keys
-
post-generation menu
-
extension cancel flow
-
quit flow
-
loading lock handling
-
global shortcuts
-
focus navigation
-
submit flow
-
extension flow
-
metadata selectors
-
text input
-
-
Generation lifecycle semantics from
GenerationFlowCoordinatorremain unchanged:-
token-based stale completion protection
-
cancellation semantics (
CancellationExceptionand cancel flag) -
dropped-callback reconciliation via
reconcileCompletionIfDone
-
-
Post-generation flow semantics remain unchanged:
-
action selection and visibility sub-menu behavior
-
stable exit-plan generation
-
Generate againreturns to active TUI session without quitting
-
-
Non-key event behavior remains unchanged:
-
TickEventdrives repaint/progress updates -
ResizeEventupdates status text while preserving state -
unknown events are ignored
-
-
Overlay rendering order remains fixed:
-
header/body/footer
-
generation overlay
-
command palette
-
help overlay
-
post-generation menu
-
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
UiEventRouterprecedence: help, command palette, post-generation menu, extension cancel, quit, loading lock, global shortcuts, focus navigation, submit, extension flow, metadata selectors, text input. -
Keep
Escunwind 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
UiEffecthandling. -
Route follow-up semantic state changes back through
UiIntentinstead of mutatingreducerStatedirectly from callbacks or helper methods. -
Keep mutable widget internals (
TextInputState,ListState, terminal handles) outsideUiState. -
If a change touches form authority, keep
ProjectRequestandValidationReportsynchronized through reducer-owned state.
Native Release Changes
-
Preserve CI native-size gating in
.github/workflows/ci.ymlfor both-Pheadless,nativeand full-Pnativebuilds. -
Update
scripts/CheckNativeSize.javatests 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 fromcli,headless,runtime,postgen,persistence, andui. -
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
RuntimeServicesmay cross fromruntime/intoheadless/to open the headless generation service. -
UI classes must not depend on
cli,headless, orruntime. -
Post-generation classes must not depend on
cli,headless, orpersistence. -
TuiSessionSummarylives inpostgen/, so post-generation actions no longer depend back onruntime/. -
Persistence classes must not depend on
cli,headless,runtime,ui, orpostgen. -
Core non-UI layers (
application,api,archive,diagnostics,domain,forge,headless,persistence,util) must not depend onui.
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. |
Sealed Types |
|
Immutable Snapshots |
|
DRY |
|
Derived State Elimination |
|
Single Source of Truth |
Reducer-visible TUI semantics are synchronized onto |
Encapsulation |
|
Resource Safety |
|
Atomic Persistence |
|
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()andDuration.ZEROdebounce for deterministic behavior -
Property-based tests for ZIP extraction edge cases
See Testing Strategy for details.