This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-887-dev-tooling-bundle
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 601488ff32a9ad301ad4a4c0ce076c02eea1d27b
Author: Serge Huber <[email protected]>
AuthorDate: Sun Jun 28 09:02:23 2026 +0200

    UNOMI-887: Align build.ps1 with build.sh and add generic backport agent 
docs.
    
    Bring the Windows build script to feature parity with build.sh, add ASF and
    PowerShell help headers, and layer generic branch-backport guidance under 
the
    Unomi-specific backport rules for agents and Cursor.
---
 .cursor/rules/branch-backport.mdc      |   90 +++
 .cursor/rules/unomi-3-dev-backport.mdc |   60 +-
 AGENTS.md                              |   47 +-
 build.ps1                              | 1233 +++++++++++++++++++-------------
 4 files changed, 879 insertions(+), 551 deletions(-)

diff --git a/.cursor/rules/branch-backport.mdc 
b/.cursor/rules/branch-backport.mdc
new file mode 100644
index 000000000..aa4eb00b7
--- /dev/null
+++ b/.cursor/rules/branch-backport.mdc
@@ -0,0 +1,90 @@
+---
+description: Generic workflow for backporting changes from a source branch to 
a target branch without regressions
+alwaysApply: true
+---
+
+# Branch backport rules (generic — mandatory)
+
+Use whenever porting work **from a source branch to a target branch** (release 
line, long-lived dev branch, stacked PR parent, etc.).
+
+Set branch names once per task (examples: `SOURCE=feature/x` `TARGET=main`, or 
`SOURCE=unomi-3-dev` `TARGET=master`):
+
+```bash
+cd "$(git rev-parse --show-toplevel)"
+git fetch origin
+SOURCE=<source-branch>   # where the change lives today
+TARGET=<target-branch>   # where it must land (canonical for existing files)
+```
+
+Repo-specific overlays (e.g. UNOMI-875): 
`.cursor/rules/unomi-3-dev-backport.mdc` — read **after** this file when 
applicable.
+
+## Never do this
+
+- **Never** `git checkout origin/$SOURCE -- <path>` on files that **already 
exist on** `origin/$TARGET`.
+- **Never** assume “file differs on source ⇒ target is missing it” — the 
target often has **review-only fixes** the source never received.
+- **Never** `git cherry-pick` commits from `$SOURCE` history onto `$TARGET` 
for bulk backport work without per-commit review (source history is often 
pre-merge noise).
+- **Never** downgrade pinned versions on `$TARGET` (images, BOM, lockfiles) to 
match `$SOURCE` without explicit review.
+
+## Always do this
+
+1. **Branch from** `origin/$TARGET` (or current stack parent), not from 
`$SOURCE`.
+2. **Inventory:** `git diff --stat origin/$TARGET origin/$SOURCE -- <paths>` — 
size only; not a port list.
+3. **Commit history audit** on every path that exists on `$TARGET` — see below.
+4. **Classify each path:** ADD (new on target) · MODIFY (exists on target) · 
MERGE (build/CI/deps — both sides changed).
+5. **Port:**
+   - **ADD:** `git checkout origin/$SOURCE -- <new-file>` is OK if the file is 
absent on `$TARGET`.
+   - **MODIFY:** start from `origin/$TARGET`; apply additive hunks from 
`$SOURCE` by hand.
+   - **MERGE:** never replace whole file; keep `$TARGET`-only lines while 
adding `$SOURCE` features.
+6. **Pre-commit diff audit** on every modified existing file — see below.
+7. **Build/test** affected modules before opening PR; state in PR body that 
history + diff audits were run.
+
+## Commit history audit (MODIFY / MERGE)
+
+> Read **`$TARGET` history** to protect review work. Do **not** use `$SOURCE` 
commit SHAs as a port shortcut.
+
+```bash
+PATH=path/to/file
+
+# Target-only commits (review PRs, bugfixes never merged to source):
+git log origin/$SOURCE..origin/$TARGET --oneline -- "$PATH"
+
+# Recent target context (PR numbers in subjects):
+git log origin/$TARGET -20 --oneline -- "$PATH"
+
+# If your branch deletes lines, trace them on target:
+git diff origin/$TARGET -- "$PATH"
+git blame origin/$TARGET -L <start>,<end> -- "$PATH"
+git log -1 --format='%h %s (%ci)' <commit-from-blame>
+```
+
+| Signal | Interpret |
+|---|---|
+| Non-empty `origin/$SOURCE..origin/$TARGET` log | Target has fixes source 
lacks — **do not overwrite** file from source |
+| Empty target-only log | Still run diff audit; may be safe ADD or symmetric 
drift |
+| Non-empty target-only log **and** substantive removals in your branch | 
**STOP** — read those commits before continuing |
+
+Optional: `git log origin/$TARGET..origin/$SOURCE --oneline -- "$PATH"` lists 
what source added — candidate material, **not** a license to blind checkout.
+
+## Pre-commit diff regression audit
+
+```bash
+PATH=path/to/file
+
+# What target has that source lacks (fixes we must keep):
+git diff origin/$SOURCE origin/$TARGET -- "$PATH"
+
+# What our branch removes from target (should be empty or whitespace/comments 
only):
+git diff origin/$TARGET -- "$PATH" | rg '^-' | rg -v '^---|^-import |^-package 
|^-\s*$|^- \*|^-\s*/\*|^-\s*\*'
+
+# Gate: substantive removal count (threshold ~5 → investigate)
+git diff origin/$TARGET -- "$PATH" | rg '^-' | rg -v '^---|^-import |^-package 
|^-\s*$|^- \*|^-\s*/\*|^-\s*\*' | wc -l
+```
+
+Cross-check: if target-only log is non-empty **and** removal count > 0, read 
target-only commits before committing.
+
+## Validation before PR
+
+- [ ] `SOURCE` / `TARGET` named in PR description
+- [ ] Commit history audit for every modified **existing** file
+- [ ] No substantive `-` lines vs `origin/$TARGET` on modified existing files
+- [ ] Module compile / targeted tests for touched areas
diff --git a/.cursor/rules/unomi-3-dev-backport.mdc 
b/.cursor/rules/unomi-3-dev-backport.mdc
index 895312ec7..c69e9fa10 100644
--- a/.cursor/rules/unomi-3-dev-backport.mdc
+++ b/.cursor/rules/unomi-3-dev-backport.mdc
@@ -1,49 +1,55 @@
 ---
-description: Mandatory workflow for backporting unomi-3-dev → master 
(UNOMI-875 Phase 2)
+description: UNOMI-875 overlay — backporting unomi-3-dev → master (extends 
branch-backport.mdc)
 alwaysApply: true
 ---
 
-# unomi-3-dev → master backport rules (mandatory)
+# unomi-3-dev → master (UNOMI-875 Phase 2)
 
-Epic: **UNOMI-875**. Source branch: `unomi-3-dev`. Target: `master`.
+**Generic workflow first:** `.cursor/rules/branch-backport.mdc` (history 
audit, diff audit, ADD/MODIFY/MERGE).
 
-Local plan (git-ignored): `.local-notes/unomi-3-dev-backport-plan-phase2.md`
+For this epic, set:
 
-## Never do this
+```bash
+SOURCE=unomi-3-dev
+TARGET=master
+```
 
-- **Never** `git checkout origin/unomi-3-dev -- <path>` on files that 
**already exist on master** (especially Java, CI, docker compose, `build.sh`).
-- **Never** port by commit hash from `unomi-3-dev` history.
-- **Never** replace `master` review refactors with 3-dev monoliths (REST 
mappers, tracing, test harness, shell CRUD fixes).
-- **Never** downgrade pinned versions on master (OpenSearch/Elasticsearch 
images, `pom.xml` properties) to match 3-dev without explicit review.
+Local tracker (git-ignored): 
`.local-notes/unomi-3-dev-backport-plan-phase2.md` — §2e playbook, §1 exclusion 
register.
 
-## Always do this
+## Unomi-specific never
 
-1. **Assume master may be ahead.** A diff vs `unomi-3-dev` ≠ missing feature. 
Check merged PRs (#771–#783, etc.) first.
-2. **Start from master** for every modified file: read `origin/master` 
version, then cherry-pick additive hunks from 3-dev.
-3. **Before staging**, run regression audit on touched paths:
+- **Never** replace `master` review refactors with 3-dev monoliths (REST 
mappers, tracing lifecycle, test harness, shell CRUD fixes).
+- **Never** port items in plan §1 exclusion register: REST exception mappers 
(implement on master patterns), migration scripts (UNOMI-943), security WIP, 
wholesale 3-dev test harness.
+- **Never** wholesale-checkout `.github/workflows` (master ahead: #780, #957), 
`AGENTS.md` (#769), migration groovy, `.asf.yaml` rulesets.
 
-```bash
-cd "$(git rev-parse --show-toplevel)"
-git fetch origin
-# Master-only lines we must not drop:
-git diff origin/unomi-3-dev origin/master -- <path>
-# What our branch removes from master (should be empty or whitespace-only):
-git diff origin/master -- <path> | rg '^-' | rg -v '^---|^-import |^-package 
|^-\s*$|^- \*|^-\s*/\*|^-\s*\*'
-```
+## Unomi-specific always
 
-4. **Safe to add wholesale** (new on master): new scripts, Postman JSON, docs, 
`build.ps1`, setup/clear helpers, new Java classes (e.g. 
`DistributionCompleter`).
-5. **Merge surgically**: `build.sh`, `pom.xml`, CI workflows — keep master 
CI/non-interactive, Javadoc, IT memory sampler (#780, #957).
-6. **Exclusion register** (master wins): see plan §1 — REST mappers, 
`EventServiceImpl`, `ParserHelper`, security WIP, migration scripts 
(UNOMI-943), etc.
+- Check merged Phase 2 PRs (#771–#783, #755, #763, etc.) before assuming 
master lacks a 3-dev feature.
+- **MERGE surgically:** `build.sh`, root `pom.xml`, CI — keep master 
non-interactive CI, Javadoc, IT memory sampler (#780, #957).
+- **Docker compose:** additive only (`container_name`, debug ports); **keep** 
master OpenSearch/ES image pins (3-dev had 3.4.0 regressions vs master 3.0.0).
 
 ## Shell CRUD (PR J) — learned 2026-06-10
 
-Shell CSV/table polish from 3-dev is **mostly already on master** (#755, 
#763). Remaining 3-dev shell diffs often **revert** review fixes 
(UndeployDefinition, ConsentCrudCommand, ApiKeyCrudCommand, RuleCrudCommand, 
docker OpenSearch pins). Port only:
+Shell CSV/table polish from 3-dev is **mostly already on master** (#755, 
#763). Remaining 3-dev shell diffs often **revert** review fixes 
(`UndeployDefinition`, `ConsentCrudCommand`, `ApiKeyCrudCommand`, 
`RuleCrudCommand`, docker pins). Port only:
 
 - New classes (`DistributionCompleter`)
 - Additive CLI flags (`Setup --show`, `@Completion`) on top of master defaults
 
-## Validation before PR
+**PR 1 incident:** `git log origin/unomi-3-dev..origin/master -- 
tools/shell-dev-commands/` would have flagged #755/#763 before blind checkout 
reverted ~20 fixes.
+
+## Per–mega-PR risk (plan §2e)
+
+| PR | Extra risk |
+|---|---|
+| **2** (V+U) | Wrong feature name breaks Karaf boot — diff `feature.xml` vs 
master names |
+| **3** (L–P) | Security + persistence — never replace mappers/tracing |
+| **4** (F3–F4) | Extend master `AbstractRestExceptionMapper`; do not copy 
3-dev monoliths |
+| **5** (K) | Router only — never bundle; never blind-checkout 
`extensions/router/**` |
+
+## Validation before PR (Unomi)
+
+Run generic checklist from `branch-backport.mdc`, plus:
 
 - `./build.sh --help`
 - `mvn -pl <affected-modules> -am compile -DskipTests`
-- Regression scan: no substantive `-` lines vs `origin/master` on modified 
existing files
+- PR description: “master-first backport; branch-backport + §2e history/diff 
audit run”
diff --git a/AGENTS.md b/AGENTS.md
index b47b552eb..8f8798315 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,14 +26,45 @@ Security model: [SECURITY.md](./SECURITY.md)
 Agents that scan this repository should consult `SECURITY.md` and the
 threat model it links before reporting issues.
 
-## Backporting from `unomi-3-dev` (UNOMI-875)
+## Branch backporting (generic)
 
-When porting Phase 2 work from `unomi-3-dev` to `master`:
+When porting changes **from a source branch to a target branch**, follow
+`.cursor/rules/branch-backport.mdc`. Summary:
 
-1. **Master is often ahead** — merged review PRs (#771–#783) improved code 
beyond 3-dev. A file diff does not mean master is missing the feature.
-2. **Never blind-checkout** existing files from `unomi-3-dev`. Start from 
`origin/master` and cherry-pick additive hunks only.
-3. **Audit before commit** — compare `origin/master` vs your branch; any 
removed logic from master is likely a regression (see 
`.cursor/rules/unomi-3-dev-backport.mdc`).
-4. **Safe wholesale adds**: new scripts, docs, Postman collection, new Java 
classes. **Surgical merge**: `build.sh`, `pom.xml`, CI workflows (keep master 
CI/Javadoc/IT memory sampling).
-5. **Do not port** items in the plan exclusion register (REST mappers, 
migration scripts UNOMI-943, security WIP, wholesale test harness from 3-dev).
+1. **Target is canonical** for files that already exist there — branch from
+   `origin/<target>`, not from source.
+2. **Target may be ahead** — merged review PRs on the target often improve
+   code beyond the source. A diff ≠ missing feature.
+3. **Commit history on the target** — for each existing file:
+   `git log origin/<source>..origin/<target> -- <path>`. Non-empty output
+   means target-only fixes you must not drop.
+4. **Never blind-checkout** existing files from source
+   (`git checkout origin/<source> -- <path>`). Port additive hunks only.
+5. **Audit before commit** — diff regression scan plus history cross-check;
+   use `git blame origin/<target>` on any line you remove.
+6. **Classify paths:** ADD (new on target) · MODIFY (hand-merge from
+   target) · MERGE (build/CI/deps — target wins on target-only lines).
+7. **Do not** bulk-cherry-pick from source branch history without
+   per-commit review.
 
-Detailed tracker: `.local-notes/unomi-3-dev-backport-plan-phase2.md` (local, 
git-ignored).
+Cursor rule: `.cursor/rules/branch-backport.mdc`
+
+## Backporting `unomi-3-dev` → `master` (UNOMI-875)
+
+Active Phase 2 epic. **Apply generic rules above first**, then
+`.cursor/rules/unomi-3-dev-backport.mdc` for Unomi-specific exclusions and
+mega-PR risks.
+
+| | |
+|---|---|
+| Source | `unomi-3-dev` |
+| Target | `master` |
+| Local plan | `.local-notes/unomi-3-dev-backport-plan-phase2.md` 
(git-ignored) |
+
+Unomi-specific reminders:
+
+- Master is ahead on REST mappers, tracing, shell CRUD (#755, #763), CI
+  (#780, #957), docker image pins — do not revert.
+- Safe wholesale adds: new scripts, Postman, docs, new Java classes.
+- Do not port: migration scripts (UNOMI-943), 3-dev REST mappers, security
+  WIP, wholesale test harness from 3-dev.
diff --git a/build.ps1 b/build.ps1
index bed45c6fd..85c024fea 100644
--- a/build.ps1
+++ b/build.ps1
@@ -5,7 +5,7 @@
 # (the "License"); you may not use this file except in compliance with
 # the License.  You may obtain a copy of the License at
 #
-#      http://www.apache.org/licenses/LICENSE-2.0
+#       http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,16 +13,45 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Error handling
-$ErrorActionPreference = "Stop"
-$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
+################################################################################
+#
+#  Apache Unomi build script for Windows (PowerShell equivalent of build.sh)
+#
+################################################################################
+
+<#
+.SYNOPSIS
+    Builds, tests, deploys, and runs Apache Unomi on Windows.
+
+.DESCRIPTION
+    Windows counterpart to ./build.sh. Supports the same build modes: 
unit/integration
+    tests, OpenSearch/ElasticSearch IT profiles, Karaf debug, deployment, CI 
mode,
+    Javadoc validation, and integration-test tracing. Run with -Help for all 
options.
+
+    Requires: Java 11+, Maven 3.6+, GraphViz (dot), tar (Windows 10+ or Git 
for Windows).
+    Integration tests additionally require Docker Desktop.
+
+.EXAMPLE
+    .\build.ps1 -Help
+
+.EXAMPLE
+    .\build.ps1 -IntegrationTests -UseOpenSearch
+
+.EXAMPLE
+    .\build.ps1 -NoKaraf -SkipTests
+
+.NOTES
+    Pair with optional setenv.ps1 in the repo root (same role as setenv.sh).
+    See manual/src/main/asciidoc/building-and-deploying.adoc (build.sh 
section).
+#>
 
-# Script parameters
 param(
     [switch]$Help,
     [switch]$MavenDebug,
+    [switch]$MavenQuiet,
     [switch]$Offline,
     [switch]$SkipTests,
+    [switch]$SkipUnitTests,
     [switch]$IntegrationTests,
     [switch]$Deploy,
     [switch]$Debug,
@@ -32,640 +61,812 @@ param(
     [switch]$PurgeMavenCache,
     [string]$KarafHome,
     [switch]$UseOpenSearch,
+    [string]$Distribution,
+    [string]$SearchHeap,
+    [string]$KarafHeap,
     [switch]$NoKaraf,
     [string]$AutoStart,
-    [switch]$ResolverDebug
+    [string]$SingleTest,
+    [switch]$ItDebug,
+    [int]$ItDebugPort = 5006,
+    [switch]$ItDebugSuspend,
+    [switch]$SkipMigrationTests,
+    [switch]$ResolverDebug,
+    [switch]$KeepContainer,
+    [switch]$NoMemorySampler,
+    [int]$MemoryInterval = 30,
+    [switch]$Javadoc,
+    [string]$LogFile,
+    [switch]$LogFileOnly,
+    [switch]$Ci
 )
 
-# Global variables
-$script:HasColors = $Host.UI.SupportsVirtualTerminal
-$script:StartTime = $null
+$ErrorActionPreference = 'Stop'
+$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
 
-# ANSI color codes (only used if terminal supports them)
-if ($HasColors) {
-    $script:RED = "`e[0;31m"
-    $script:GREEN = "`e[0;32m"
-    $script:YELLOW = "`e[1;33m"
-    $script:BLUE = "`e[0;34m"
-    $script:MAGENTA = "`e[0;35m"
-    $script:CYAN = "`e[0;36m"
-    $script:GRAY = "`e[0;90m"
-    $script:BOLD = "`e[1m"
-    $script:NC = "`e[0m"
-    
-    # Unicode symbols
-    $script:CHECK_MARK = "✓"
-    $script:CROSS_MARK = "✗"
-    $script:ARROW = "➜"
-    $script:WARNING = "⚠"
-    $script:INFO = "ℹ"
-} else {
-    # Plain text alternatives
-    $script:CHECK_MARK = "(+)"
-    $script:CROSS_MARK = "(x)"
-    $script:ARROW = "(>)"
-    $script:WARNING = "(!)"
-    $script:INFO = "(i)"
+# Preserve invocation for IT run tracing (archive-it-run.sh reads this on Unix)
+$script:BuildScriptInvocation = @($MyInvocation.Line)
+
+# Defaults aligned with build.sh
+$script:UseMavenCache = -not $NoMavenCache
+$script:ItMemorySampler = -not $NoMemorySampler
+$script:KarafDebugSuspend = if ($DebugSuspend) { 'y' } else { 'n' }
+
+if ($Ci) {
+    $NoKaraf = $true
+    $script:UseMavenCache = $false
+    $env:BUILD_NON_INTERACTIVE = 'true'
+    $MavenQuiet = $true
+    $Javadoc = $true
+}
+
+if ($PurgeMavenCache) {
+    $script:UseMavenCache = $false
+}
+
+if ($UseOpenSearch -and [string]::IsNullOrWhiteSpace($Distribution)) {
+    $Distribution = 'unomi-distribution-opensearch'
+}
+if (-not [string]::IsNullOrWhiteSpace($Distribution) -and $Distribution -like 
'*opensearch*') {
+    $UseOpenSearch = $true
 }
 
-# Helper functions
+if ($LogFileOnly -and [string]::IsNullOrWhiteSpace($LogFile)) {
+    Write-Error '--log-file-only requires --log-file PATH'
+    exit 1
+}
+
+function Test-NonInteractive {
+    return [bool]($env:CI -or $env:GITHUB_ACTIONS -or 
$env:BUILD_NON_INTERACTIVE -eq 'true')
+}
+
+function Initialize-Colors {
+    $script:HasColors = $false
+    if ($env:NO_COLOR) { return }
+    if (-not [Console]::IsOutputRedirected) {
+        $script:HasColors = $Host.UI.SupportsVirtualTerminal
+    }
+
+    if ($script:HasColors) {
+        $script:RED = "`e[0;31m"
+        $script:GREEN = "`e[0;32m"
+        $script:YELLOW = "`e[1;33m"
+        $script:BLUE = "`e[0;34m"
+        $script:MAGENTA = "`e[0;35m"
+        $script:CYAN = "`e[0;36m"
+        $script:GRAY = "`e[0;90m"
+        $script:BOLD = "`e[1m"
+        $script:NC = "`e[0m"
+        $script:CHECK_MARK = '✔'
+        $script:CROSS_MARK = '✘'
+        $script:ARROW = '➜'
+        $script:WARNING = '⚠'
+        $script:INFO = 'ℹ'
+    } else {
+        $script:RED = $script:GREEN = $script:YELLOW = $script:BLUE = ''
+        $script:MAGENTA = $script:CYAN = $script:GRAY = $script:BOLD = 
$script:NC = ''
+        $script:CHECK_MARK = '(+)'
+        $script:CROSS_MARK = '(x)'
+        $script:ARROW = '(>)'
+        $script:WARNING = '(!)'
+        $script:INFO = '(i)'
+    }
+}
+
+Initialize-Colors
+
 function Write-Section {
     param([string]$Text)
-    
     $totalWidth = 80
     $textLength = $Text.Length
     $paddingLength = [math]::Floor(($totalWidth - $textLength - 4) / 2)
-    $leftPadding = " " * $paddingLength
-    $rightPadding = " " * ($paddingLength + ($totalWidth - $textLength - 4) % 
2)
-    
-    Write-Host ""
-    if ($HasColors) {
-        Write-Host 
"$BLUE╔════════════════════════════════════════════════════════════════════════════╗$NC"
-        Write-Host "$BLUE║$NC$leftPadding$BOLD$Text$NC$rightPadding$BLUE║$NC"
-        Write-Host 
"$BLUE╚════════════════════════════════════════════════════════════════════════════╝$NC"
+    $leftPadding = ' ' * $paddingLength
+    $rightPadding = ' ' * ($paddingLength + (($totalWidth - $textLength - 4) % 
2))
+    Write-Host ''
+    if ($script:HasColors) {
+        Write-Host 
"$($script:BLUE)╔════════════════════════════════════════════════════════════════════════════╗$($script:NC)"
+        Write-Host 
"$($script:BLUE)║$($script:NC)$leftPadding$($script:BOLD)$Text$($script:NC)$rightPadding$($script:BLUE)║$($script:NC)"
+        Write-Host 
"$($script:BLUE)╚════════════════════════════════════════════════════════════════════════════╝$($script:NC)"
     } else {
-        Write-Host 
"+------------------------------------------------------------------------------+"
+        Write-Host 
'+------------------------------------------------------------------------------+'
         Write-Host "| $leftPadding$Text$rightPadding |"
-        Write-Host 
"+------------------------------------------------------------------------------+"
+        Write-Host 
'+------------------------------------------------------------------------------+'
     }
 }
 
 function Write-Status {
-    param(
-        [string]$Status,
-        [string]$Message
-    )
-    
+    param([string]$Status, [string]$Message)
     $symbol = switch ($Status) {
-        "success" { if ($HasColors) { "$GREEN$CHECK_MARK$NC" } else { 
$CHECK_MARK } }
-        "error"   { if ($HasColors) { "$RED$CROSS_MARK$NC" } else { 
$CROSS_MARK } }
-        "warning" { if ($HasColors) { "$YELLOW$WARNING$NC" } else { $WARNING } 
}
-        "info"    { if ($HasColors) { "$CYAN$INFO$NC" } else { $INFO } }
-        default   { if ($HasColors) { "$BLUE$ARROW$NC" } else { $ARROW } }
+        'success' { $script:CHECK_MARK }
+        'error'   { $script:CROSS_MARK }
+        'warning' { $script:WARNING }
+        'info'    { $script:INFO }
+        default   { $script:ARROW }
     }
-    
     $color = switch ($Status) {
-        "success" { $GREEN }
-        "error"   { $RED }
-        "warning" { $YELLOW }
-        "info"    { $CYAN }
-        default   { "" }
-    }
-    
-    if ($HasColors) {
-        Write-Host " $symbol $color$Message$NC"
+        'success' { $script:GREEN }
+        'error'   { $script:RED }
+        'warning' { $script:YELLOW }
+        'info'    { $script:CYAN }
+        default   { '' }
+    }
+    if ($script:HasColors -and $color) {
+        Write-Host " $symbol ${color}${Message}$($script:NC)"
     } else {
         Write-Host " $symbol $Message"
     }
 }
 
-function Write-Progress {
-    param(
-        [int]$Current,
-        [int]$Total,
-        [string]$Message
-    )
-    
+function Write-BuildProgress {
+    param([int]$Current, [int]$Total, [string]$Message)
     $percentage = [math]::Floor(($Current * 100) / $Total)
-    
-    if ($HasColors) {
+    if ($script:HasColors) {
         $filled = [math]::Floor($percentage / 2)
         $empty = 50 - $filled
-        
-        $progress = "["
-        $progress += "█" * $filled
-        $progress += "░" * $empty
-        $progress += "]"
-        
-        Write-Host "`r$CYAN$progress$NC $percentage% $GRAY$Message$NC" 
-NoNewline
+        $bar = '[' + ('█' * $filled) + ('░' * $empty) + ']'
+        Write-Host "`r$($script:CYAN)$bar$($script:NC) $percentage% 
$($script:GRAY)$Message$($script:NC)" -NoNewline
     } else {
         $filled = [math]::Floor($percentage / 4)
         $empty = 25 - $filled
-        
-        $progress = "["
-        $progress += "#" * $filled
-        $progress += "-" * $empty
-        $progress += "]"
-        
-        Write-Host "`r$progress $percentage% $Message" -NoNewline
+        $bar = '[' + ('#' * $filled) + ('-' * $empty) + ']'
+        Write-Host "`r$bar $percentage% $Message" -NoNewline
     }
 }
 
-function Test-Command {
+function Test-CommandExists {
     param([string]$Name)
     return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue)
 }
 
-function Get-ElapsedTime {
-    if ($null -eq $script:StartTime) { return "00:00" }
-    $elapsed = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
-    return "{0:d2}:{1:d2}" -f ($elapsed / 60), ($elapsed % 60)
-}
-
-function Start-Timer {
-    $script:StartTime = Get-Date
+function Invoke-PromptContinue {
+    param([string]$PromptText = 'Continue?')
+    if (Test-NonInteractive) {
+        Write-Status 'info' "Non-interactive mode: continuing ($PromptText)"
+        return
+    }
+    $reply = Read-Host "$PromptText (y/N)"
+    if ($reply -notmatch '^[Yy]$') { exit 1 }
 }
 
 function Show-Usage {
-    if ($HasColors) {
-        Write-Host $CYAN
-        Write-Host @"
+    Write-Section 'Apache Unomi Build Script'
+    if ($script:HasColors) { Write-Host $script:CYAN }
+    Write-Host @'
      _    _ _____ _      ____
     | |  | |  ___| |    |  _ \
     | |__| | |__ | |    | |_) |
     |  __  |  __|| |    |  __/
     | |  | | |___| |____| |
     |_|  |_\_____|______|_|
-"@
-        Write-Host $NC
-    } else {
-        Write-Host @"
-     _    _ _____ _      ____
-    | |  | |  ___| |    |  _ \
-    | |__| | |__ | |    | |_) |
-    |  __  |  __|| |    |  __/
-    | |  | | |___| |____| |
-    |_|  |_\_____|______|_|
-"@
-    }
-    
-    Write-Host "Usage: build.ps1 [options]"
-    Write-Host ""
-    Write-Host "Options:"
-    if ($HasColors) {
-        Write-Host "  $CYAN-Help$NC                    Show this help message"
-        Write-Host "  $CYAN-MavenDebug$NC             Enable Maven debug 
output"
-        Write-Host "  $CYAN-Offline$NC                Run Maven in offline 
mode"
-        Write-Host "  $CYAN-SkipTests$NC              Skip all tests"
-        Write-Host "  $CYAN-IntegrationTests$NC       Run integration tests"
-        Write-Host "  $CYAN-Deploy$NC                 Deploy after build"
-        Write-Host "  $CYAN-Debug$NC                  Run Karaf in debug mode"
-        Write-Host "  $CYAN-DebugPort$NC <port>       Set debug port (default: 
5005)"
-        Write-Host "  $CYAN-DebugSuspend$NC          Suspend JVM until 
debugger connects"
-        Write-Host "  $CYAN-NoMavenCache$NC           Disable Maven build 
cache"
-        Write-Host "  $CYAN-PurgeMavenCache$NC        Purge local Maven cache 
before building"
-        Write-Host "  $CYAN-KarafHome$NC <path>       Set Karaf home directory 
for deployment"
-        Write-Host "  $CYAN-UseOpenSearch$NC         Use OpenSearch instead of 
ElasticSearch"
-        Write-Host "  $CYAN-NoKaraf$NC              Build without starting 
Karaf"
-        Write-Host "  $CYAN-AutoStart$NC <engine>    Auto-start with specified 
engine"
-        Write-Host "  $CYAN-ResolverDebug$NC        Enable Karaf Resolver 
debug logging for integration tests"
-    } else {
-        Write-Host "  -Help                    Show this help message"
-        Write-Host "  -MavenDebug             Enable Maven debug output"
-        Write-Host "  -Offline                Run Maven in offline mode"
-        Write-Host "  -SkipTests              Skip all tests"
-        Write-Host "  -IntegrationTests       Run integration tests"
-        Write-Host "  -Deploy                 Deploy after build"
-        Write-Host "  -Debug                  Run Karaf in debug mode"
-        Write-Host "  -DebugPort <port>       Set debug port (default: 5005)"
-        Write-Host "  -DebugSuspend          Suspend JVM until debugger 
connects"
-        Write-Host "  -NoMavenCache           Disable Maven build cache"
-        Write-Host "  -PurgeMavenCache        Purge local Maven cache before 
building"
-        Write-Host "  -KarafHome <path>       Set Karaf home directory for 
deployment"
-        Write-Host "  -UseOpenSearch         Use OpenSearch instead of 
ElasticSearch"
-        Write-Host "  -NoKaraf              Build without starting Karaf"
-        Write-Host "  -AutoStart <engine>    Auto-start with specified engine"
-        Write-Host "  -ResolverDebug         Enable Karaf Resolver debug 
logging for integration tests"
-    }
-    
-    Write-Host ""
-    Write-Host "Examples:"
-    if ($HasColors) {
-        Write-Host "$GRAY# Build with integration tests using OpenSearch$NC"
-        Write-Host "$GRAY.\build.ps1 -IntegrationTests -UseOpenSearch$NC"
-        Write-Host ""
-        Write-Host "$GRAY# Build in debug mode$NC"
-        Write-Host "$GRAY.\build.ps1 -Debug -DebugPort 5006 -DebugSuspend$NC"
-        Write-Host ""
-        Write-Host "$GRAY# Deploy to specific Karaf instance$NC"
-        Write-Host "$GRAY.\build.ps1 -Deploy -KarafHome ~\apache-karaf$NC"
-    } else {
-        Write-Host "# Build with integration tests using OpenSearch"
-        Write-Host ".\build.ps1 -IntegrationTests -UseOpenSearch"
-        Write-Host ""
-        Write-Host "# Build in debug mode"
-        Write-Host ".\build.ps1 -Debug -DebugPort 5006 -DebugSuspend"
-        Write-Host ""
-        Write-Host "# Deploy to specific Karaf instance"
-        Write-Host ".\build.ps1 -Deploy -KarafHome ~\apache-karaf"
-    }
-    
+'@
+    if ($script:HasColors) { Write-Host $script:NC }
+    Write-Host ''
+    Write-Host 'Usage: .\build.ps1 [options]'
+    Write-Host ''
+    Write-Host 'Options:'
+    Write-Host '  -Help                      Show this help message'
+    Write-Host '  -SkipTests                 Skip all tests'
+    Write-Host '  -SkipUnitTests             Skip unit tests (integration 
tests can still run)'
+    Write-Host '  -IntegrationTests          Run integration tests'
+    Write-Host '  -Deploy                    Deploy after build'
+    Write-Host '  -MavenDebug                Enable Maven debug output'
+    Write-Host '  -MavenQuiet                Disable Maven download progress 
(quiet mode)'
+    Write-Host '  -Offline                   Run Maven in offline mode'
+    Write-Host '  -Debug                     Run Karaf in debug mode'
+    Write-Host '  -DebugPort PORT            Set debug port (default: 5005)'
+    Write-Host '  -DebugSuspend              Suspend JVM until debugger 
connects'
+    Write-Host '  -NoMavenCache              Disable Maven build cache'
+    Write-Host '  -PurgeMavenCache           Purge local Maven cache before 
building'
+    Write-Host '  -KarafHome PATH            Set Karaf home directory for 
deployment'
+    Write-Host '  -UseOpenSearch             Use OpenSearch instead of 
ElasticSearch'
+    Write-Host '  -Distribution DIST         Set Unomi distribution (e.g. 
unomi-distribution-opensearch)'
+    Write-Host '  -SearchHeap SIZE           Search engine heap for 
integration tests'
+    Write-Host '  -KarafHeap SIZE            Karaf JVM heap for integration 
tests'
+    Write-Host '  -NoKaraf                   Build without starting Karaf'
+    Write-Host '  -AutoStart ENGINE          Auto-start elasticsearch or 
opensearch'
+    Write-Host '  -SingleTest TEST           Run a single integration test'
+    Write-Host '  -ItDebug                   Enable integration test debug 
mode'
+    Write-Host '  -ItDebugPort PORT          Set integration test debug port 
(default: 5006)'
+    Write-Host '  -ItDebugSuspend            Suspend integration test until 
debugger connects'
+    Write-Host '  -SkipMigrationTests        Skip migration-related tests'
+    Write-Host '  -ResolverDebug             Enable Karaf Resolver debug 
logging for integration tests'
+    Write-Host '  -KeepContainer             Keep search engine container 
running after tests'
+    Write-Host '  -NoMemorySampler           Disable JVM/system memory 
sampling during integration tests'
+    Write-Host '  -MemoryInterval SEC        Memory sample interval in seconds 
(default: 30)'
+    Write-Host '  -Javadoc                   Build and validate Javadoc after 
install'
+    Write-Host '  -Ci                        CI mode: no Karaf, no Maven 
cache, non-interactive, Javadoc'
+    Write-Host '  -LogFile PATH              Tee all output to PATH (console + 
file)'
+    Write-Host '  -LogFileOnly               With -LogFile: write to file 
only, suppress console'
+    Write-Host ''
+    Write-Host 'Examples:'
+    Write-Host '  .\build.ps1 -IntegrationTests -UseOpenSearch'
+    Write-Host '  .\build.ps1 -SkipUnitTests -IntegrationTests'
+    Write-Host '  .\build.ps1 -Debug -DebugPort 5006 -DebugSuspend'
+    Write-Host '  .\build.ps1 -Deploy -KarafHome C:\apache-karaf'
+    Write-Host '  .\build.ps1 -NoKaraf -AutoStart opensearch'
     exit 0
 }
 
-# Show help if requested
-if ($Help) {
-    Show-Usage
-}
+if ($Help) { Show-Usage }
 
-# Validate AutoStart parameter
 if ($AutoStart -and $AutoStart -notin @('elasticsearch', 'opensearch')) {
-    Write-Status "error" "AutoStart must be either 'elasticsearch' or 
'opensearch'"
+    Write-Error "AutoStart must be either 'elasticsearch' or 'opensearch'"
     exit 1
 }
 
-# Set environment
-$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
-$SetEnvPath = Join-Path $ScriptPath "setenv.ps1"
-if (Test-Path $SetEnvPath) {
-    . $SetEnvPath
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$SetEnvPath = Join-Path $ScriptDir 'setenv.ps1'
+if (Test-Path $SetEnvPath) { . $SetEnvPath }
+
+function Get-UserHome {
+    if ($env:USERPROFILE) { return $env:USERPROFILE }
+    if ($env:HOME) { return $env:HOME }
+    return (Get-Location).Path
+}
+
+function Initialize-ProjectVersion {
+    if ($env:UNOMI_VERSION) { return }
+    $version = (& mvn -B -q '-DforceStdout' help:evaluate 
'-Dexpression=project.version' '-DinteractiveMode=false' 2>$null | 
Out-String).Trim()
+    if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) {
+        Write-Error 'Failed to detect project version from Maven'
+        exit 1
+    }
+    $env:UNOMI_VERSION = $version
+    Write-Host "Detected project version=$($env:UNOMI_VERSION)"
 }
 
-# Purge Maven cache if requested
+Initialize-ProjectVersion
+
 if ($PurgeMavenCache) {
-    Write-Host "Purging Maven cache..."
-    Remove-Item -Force -Recurse -ErrorAction SilentlyContinue `
-        "$env:USERPROFILE\.m2\build-cache",
-        "$env:USERPROFILE\.m2\dependency-cache",
-        "$env:USERPROFILE\.m2\dependency-cache_v2"
-    Write-Host "Maven cache purged."
+    Write-Host 'Purging Maven cache...'
+    $m2 = Join-Path (Get-UserHome) '.m2'
+    foreach ($dir in @('build-cache', 'dependency-cache', 
'dependency-cache_v2')) {
+        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path 
$m2 $dir)
+    }
+    Write-Host 'Maven cache purged.'
 }
 
-function Test-JavaVersion {
-    try {
-        $javaVersion = & java -version 2>&1
-        if ($javaVersion -match 'version "(\d+)') {
-            $version = [int]$Matches[1]
-            if ($version -ge 11) {
-                Write-Status "success" "Java $version detected"
-                return $true
-            } else {
-                Write-Status "error" "Java $version detected, but version 11 
or higher is required"
-                Write-Status "info" "To install Java 11 or higher:"
-                Write-Status "info" "  - Windows (Chocolatey): choco install 
temurin11"
-                Write-Status "info" "  - Windows (Manual): Download from 
https://adoptium.net";
-                Write-Status "info" "  - Windows (Scoop): scoop install 
temurin11-jdk"
-                Write-Status "info" "For more details, see BUILDING.md in the 
project root"
-                return $false
-            }
-        }
-    } catch {
-        Write-Status "error" "Java not found"
-        Write-Status "info" "To install Java 11 or higher:"
-        Write-Status "info" "  - Windows (Chocolatey): choco install temurin11"
-        Write-Status "info" "  - Windows (Manual): Download from 
https://adoptium.net";
-        Write-Status "info" "  - Windows (Scoop): scoop install temurin11-jdk"
-        Write-Status "info" "For more details, see BUILDING.md in the project 
root"
+function Test-JavaRequirement {
+    if (-not (Test-CommandExists 'java')) {
+        Write-Status 'error' 'java not found'
+        Write-Host 'Please install Java 11 or higher from https://adoptium.net'
         return $false
     }
+    $javaVersion = & java -version 2>&1 | Out-String
+    if ($javaVersion -match 'version "(\d+)') {
+        $major = [int]$Matches[1]
+        if ($major -ge 11) {
+            Write-Status 'success' "Java $major detected"
+            return $true
+        }
+    }
+    Write-Status 'error' "Java version 11 or higher required: $javaVersion"
     return $false
 }
 
-function Test-MavenVersion {
-    try {
-        $mvnVersion = & mvn -version 2>&1
-        if ($mvnVersion -match 'Apache Maven (\d+)\.(\d+)') {
-            $major = [int]$Matches[1]
-            $minor = [int]$Matches[2]
-            if ($major -gt 3 -or ($major -eq 3 -and $minor -ge 6)) {
-                Write-Status "success" "Maven $major.$minor detected"
-                return $true
-            } else {
-                Write-Status "error" "Maven $major.$minor detected, but 
version 3.6 or higher is required"
-                Write-Status "info" "To install Maven 3.6 or higher:"
-                Write-Status "info" "  - Windows (Chocolatey): choco install 
maven"
-                Write-Status "info" "  - Windows (Manual): Download from 
https://maven.apache.org";
-                Write-Status "info" "  - Windows (Scoop): scoop install maven"
-                Write-Status "info" "For more details, see BUILDING.md in the 
project root"
-                return $false
-            }
-        }
-    } catch {
-        Write-Status "error" "Maven not found"
-        Write-Status "info" "To install Maven 3.6 or higher:"
-        Write-Status "info" "  - Windows (Chocolatey): choco install maven"
-        Write-Status "info" "  - Windows (Manual): Download from 
https://maven.apache.org";
-        Write-Status "info" "  - Windows (Scoop): scoop install maven"
-        Write-Status "info" "For more details, see BUILDING.md in the project 
root"
+function Test-MavenRequirement {
+    if (-not (Test-CommandExists 'mvn')) {
+        Write-Status 'error' 'mvn not found'
         return $false
     }
-    return $false
+    if ($Offline) {
+        Write-Status 'success' 'Maven (offline mode enabled)'
+        return $true
+    }
+    $mvnVersion = (& mvn --version | Select-Object -First 1)
+    Write-Status 'success' $mvnVersion
+    return $true
+}
+
+function Test-GraphVizRequirement {
+    if (-not (Test-CommandExists 'dot')) {
+        Write-Status 'error' 'GraphViz (dot) not found'
+        return $false
+    }
+    $dotVersion = & dot -V 2>&1 | Out-String
+    Write-Status 'success' "GraphViz: $($dotVersion.Trim())"
+    if (-not $env:GRAPHVIZ_DOT) {
+        $env:GRAPHVIZ_DOT = (Get-Command dot).Source
+    }
+    return $true
 }
 
-function Test-GraphViz {
+function Test-PortAvailable {
+    param([int]$Port)
     try {
-        $dotVersion = & dot -V 2>&1
-        if ($dotVersion -match 'graphviz version') {
-            Write-Status "success" "GraphViz detected"
-            return $true
-        }
+        $listener = 
[System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
+        $listener.Start()
+        $listener.Stop()
+        return $true
     } catch {
-        Write-Status "error" "GraphViz (dot) not found - required for 
documentation generation"
-        Write-Status "info" "To install GraphViz:"
-        Write-Status "info" "  - Windows (Chocolatey): choco install graphviz"
-        Write-Status "info" "  - Windows (Manual): Download from 
https://graphviz.org";
-        Write-Status "info" "  - Windows (Scoop): scoop install graphviz"
-        Write-Status "info" "For more details, see BUILDING.md in the project 
root"
         return $false
     }
-    return $false
 }
 
-function Test-SystemRequirements {
+function Test-DockerForIntegrationTests {
+    if (-not (Test-CommandExists 'docker')) {
+        Write-Status 'error' 'Docker is not installed or not in PATH'
+        Write-Host 'Integration tests require Docker. Install Docker Desktop 
for Windows.'
+        return $false
+    }
+    $null = & docker info 2>&1
+    if ($LASTEXITCODE -ne 0) {
+        Write-Status 'error' 'Docker is not accessible'
+        return $false
+    }
+    Write-Status 'success' "Docker available: $((& docker --version 2>&1) 
-join ' ')"
+    return $true
+}
+
+function Test-IntegrationTestEnvVars {
+    $detected = @()
+    if ($env:UNOMI_ELASTICSEARCH_CLUSTERNAME -or 
$env:UNOMI_ELASTICSEARCH_USERNAME -or $env:UNOMI_ELASTICSEARCH_PASSWORD -or
+        $env:UNOMI_ELASTICSEARCH_SSL_ENABLE -or 
$env:UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES) {
+        $detected += 'Elasticsearch'
+    }
+    if ($env:UNOMI_OPENSEARCH_CLUSTERNAME -or $env:UNOMI_OPENSEARCH_ADDRESSES 
-or $env:UNOMI_OPENSEARCH_USERNAME -or
+        $env:UNOMI_OPENSEARCH_PASSWORD -or $env:UNOMI_OPENSEARCH_SSL_ENABLE 
-or $env:UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES) {
+        $detected += 'OpenSearch'
+    }
+    if ($detected.Count -eq 0) { return }
+    Write-Status 'error' "Environment variables for $($detected -join ', ') 
are set and will interfere with integration tests"
+    Write-Host ''
+    Write-Host 'Clear them with clear-elasticsearch.sh / clear-opensearch.sh 
(Git Bash) or unset in PowerShell.'
+    exit 1
+}
+
+function Test-Requirements {
+    Write-Section 'System Requirements Check'
     $hasErrors = $false
     $hasWarnings = $false
-    
-    Write-Section "Checking System Requirements"
-    
-    # Check available memory
-    $memory = Get-CimInstance Win32_OperatingSystem
-    $availableMemoryGB = [math]::Round($memory.FreePhysicalMemory / 1MB, 2)
-    $totalMemoryGB = [math]::Round($memory.TotalVisibleMemorySize / 1MB, 2)
-    
-    if ($availableMemoryGB -lt 2) {
-        Write-Status "error" "Insufficient memory: $availableMemoryGB GB 
available, minimum 2 GB required"
-        $hasErrors = $true
-    } elseif ($availableMemoryGB -lt 4) {
-        Write-Status "warning" "Low memory: $availableMemoryGB GB available, 
recommended 4 GB or more"
+
+    Write-Status 'info' 'Checking required tools...'
+    if (-not (Test-JavaRequirement)) { $hasErrors = $true }
+    if (-not (Test-MavenRequirement)) { $hasErrors = $true }
+    if (Test-CommandExists 'tar') { Write-Status 'success' 'tar' }
+    else { Write-Status 'error' 'tar not found (required on Windows 10+ or via 
Git for Windows)'; $hasErrors = $true }
+    if (-not (Test-GraphVizRequirement)) { $hasErrors = $true }
+
+    if ($IsWindows) {
+        $os = Get-CimInstance Win32_OperatingSystem -ErrorAction 
SilentlyContinue
+        if ($os) {
+            $availableMemoryMB = [math]::Round($os.FreePhysicalMemory / 1024)
+            if ($availableMemoryMB -lt 2048) {
+                Write-Status 'warning' "Memory: ${availableMemoryMB}MB 
available (2048MB recommended)"
+                $hasWarnings = $true
+            } else {
+                Write-Status 'success' "Memory: ${availableMemoryMB}MB 
available"
+            }
+        }
+
+        $driveName = (Get-Location).Drive.Name
+        if ($driveName) {
+            $drive = Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue
+            if ($drive) {
+                $freeMB = [math]::Round($drive.Free / 1MB)
+                if ($freeMB -lt 1024) {
+                    Write-Status 'warning' "Disk space: ${freeMB}MB available 
(1024MB recommended)"
+                    $hasWarnings = $true
+                } else {
+                    Write-Status 'success' "Disk space: ${freeMB}MB available"
+                }
+            }
+        }
+    }
+
+    $settings = Join-Path (Get-UserHome) '.m2/settings.xml'
+    if (-not (Test-Path $settings)) {
+        Write-Status 'warning' 'Maven settings.xml not found'
         $hasWarnings = $true
     } else {
-        Write-Status "success" "Memory: $availableMemoryGB GB available of 
$totalMemoryGB GB total"
-    }
-    
-    # Check available disk space
-    $drive = Get-PSDrive -Name (Split-Path -Qualifier $PWD.Path)
-    $freeSpaceGB = [math]::Round($drive.Free / 1GB, 2)
-    $totalSpaceGB = [math]::Round(($drive.Free + $drive.Used) / 1GB, 2)
-    
-    if ($freeSpaceGB -lt 2) {
-        Write-Status "error" "Insufficient disk space: $freeSpaceGB GB 
available, minimum 2 GB required"
+        Write-Status 'success' 'Maven settings.xml found'
+    }
+
+    if ($Debug) {
+        if ($DebugPort -lt 1024 -or $DebugPort -gt 65535) {
+            Write-Status 'error' "Debug port: $DebugPort (invalid)"
+            $hasErrors = $true
+        } elseif (-not (Test-PortAvailable $DebugPort)) {
+            Write-Status 'error' "Debug port: $DebugPort (already in use)"
+            $hasErrors = $true
+        } else {
+            Write-Status 'success' "Debug port: $DebugPort available"
+        }
+    }
+
+    if ($Deploy) {
+        if ([string]::IsNullOrWhiteSpace($KarafHome)) {
+            Write-Status 'error' 'Karaf home directory not set for deployment'
+            $hasErrors = $true
+        } elseif (-not (Test-Path $KarafHome)) {
+            Write-Status 'error' "Karaf home directory does not exist: 
$KarafHome"
+            $hasErrors = $true
+        } elseif (-not (Test-Path (Join-Path $KarafHome 'deploy'))) {
+            Write-Status 'error' "Karaf deploy directory not found: 
$KarafHome\deploy"
+            $hasErrors = $true
+        } else {
+            Write-Status 'success' "Karaf home directory validated: $KarafHome"
+        }
+    }
+
+    if ($SkipTests -and $IntegrationTests) {
+        Write-Status 'error' 'Cannot use -SkipTests and -IntegrationTests 
together'
+        $hasErrors = $true
+    }
+    if ($SkipTests -and $SkipUnitTests) {
+        Write-Status 'error' 'Cannot use -SkipTests and -SkipUnitTests 
together'
         $hasErrors = $true
-    } elseif ($freeSpaceGB -lt 10) {
-        Write-Status "warning" "Low disk space: $freeSpaceGB GB available, 
recommended 10 GB or more"
-        $hasWarnings = $true
-    } else {
-        Write-Status "success" "Disk space: $freeSpaceGB GB available of 
$totalSpaceGB GB total"
     }
-    
-    return @{
-        HasErrors = $hasErrors
-        HasWarnings = $hasWarnings
+
+    if ($IntegrationTests -and -not (Test-DockerForIntegrationTests)) {
+        $hasErrors = $true
     }
-}
 
-function Test-Requirements {
-    $hasErrors = $false
-    $hasWarnings = $false
-    
-    Write-Section "Checking Required Tools"
-    
-    # Check Java
-    if (-not (Test-JavaVersion)) {
+    if (($UseOpenSearch -or $AutoStart -eq 'opensearch') -and 
[string]::IsNullOrWhiteSpace($env:UNOMI_OPENSEARCH_PASSWORD)) {
+        Write-Status 'error' 'UNOMI_OPENSEARCH_PASSWORD is not set for 
OpenSearch'
         $hasErrors = $true
     }
-    
-    # Check Maven
-    if (-not (Test-MavenVersion)) {
+
+    if (-not [string]::IsNullOrWhiteSpace($SingleTest) -and -not 
$IntegrationTests) {
+        Write-Status 'error' 'SingleTest specified but integration tests are 
not enabled. Use -IntegrationTests.'
+        $hasErrors = $true
+    }
+    if ($ItDebug -and -not $IntegrationTests) {
+        Write-Status 'error' 'ItDebug enabled but integration tests are not 
enabled. Use -IntegrationTests.'
         $hasErrors = $true
     }
-    
-    # Check GraphViz
-    if (-not (Test-GraphViz)) {
+    if ($ItDebug) {
+        if ($ItDebugPort -lt 1024 -or $ItDebugPort -gt 65535) {
+            Write-Status 'error' "Integration test debug port: $ItDebugPort 
(invalid)"
+            $hasErrors = $true
+        } elseif (-not (Test-PortAvailable $ItDebugPort)) {
+            Write-Status 'error' "Integration test debug port: $ItDebugPort 
(already in use)"
+            $hasErrors = $true
+        } else {
+            Write-Status 'success' "Integration test debug port: $ItDebugPort 
available"
+        }
+    }
+
+    if ($Offline -and $PurgeMavenCache) {
+        Write-Status 'error' 'Cannot use -PurgeMavenCache in offline mode'
         $hasErrors = $true
     }
-    
-    # Check system requirements
-    $sysReq = Test-SystemRequirements
-    $hasErrors = $hasErrors -or $sysReq.HasErrors
-    $hasWarnings = $hasWarnings -or $sysReq.HasWarnings
-    
-    # Final status
-    Write-Host ""
+
+    Write-Host ''
     if ($hasErrors) {
-        Write-Status "error" "One or more requirements not met. Please fix the 
errors above and try again."
-        Write-Status "info" "For more information, see BUILDING.md in the 
project root"
-        Write-Status "info" "or visit 
https://unomi.apache.org/contribute/building-and-deploying.html";
+        Write-Status 'error' 'Critical requirements not met. Please fix the 
errors above.'
         exit 1
-    } elseif ($hasWarnings) {
-        Write-Status "warning" "Requirements met with warnings. You may 
proceed, but performance might be affected."
+    }
+    if ($hasWarnings) {
+        Write-Status 'warning' 'Some non-critical requirements not met'
+        Invoke-PromptContinue 'Continue despite warnings?'
     } else {
-        Write-Status "success" "All requirements met successfully!"
+        Write-Status 'success' 'All requirements checked successfully'
     }
-    
-    Write-Host ""
 }
 
-# Check requirements before proceeding
-Test-Requirements 
+Test-Requirements
 
-# Validate OpenSearch password requirement
-if ($UseOpenSearch -or ($AutoStart -and $AutoStart -eq 'opensearch')) {
-    if (-not $env:UNOMI_OPENSEARCH_PASSWORD -or 
[string]::IsNullOrWhiteSpace($env:UNOMI_OPENSEARCH_PASSWORD)) {
-        Write-Status "error" "UNOMI_OPENSEARCH_PASSWORD is not set for 
OpenSearch"
-        Write-Status "info" "When using OpenSearch, you must set the 
UNOMI_OPENSEARCH_PASSWORD environment variable before building/starting."
-        Write-Host "Examples:"
-        Write-Host "  setx UNOMI_OPENSEARCH_PASSWORD yourStrongPassword 
(PowerShell - current user)"
-        Write-Host "  $env:UNOMI_OPENSEARCH_PASSWORD='yourStrongPassword'; 
.\build.ps1 -IntegrationTests -UseOpenSearch"
-        exit 1
+function Get-MavenArgumentList {
+    $args = @()
+    if ($MavenDebug) { $args += '-X' }
+    if ($Offline) { $args += '-o' }
+    if (-not $script:UseMavenCache) { $args += 
'-Dmaven.build.cache.enabled=false' }
+    if ($MavenQuiet) { $args += '-ntp' }
+    if ($env:MAVEN_EXTRA_OPTS) {
+        $args += $env:MAVEN_EXTRA_OPTS.Split(' ', 
[System.StringSplitOptions]::RemoveEmptyEntries)
+    }
+    if (-not [string]::IsNullOrWhiteSpace($Distribution)) {
+        $args += "-Dunomi.distribution=$Distribution"
+        Write-Host "Using Unomi distribution: $Distribution"
+    }
+    if ($env:GRAPHVIZ_DOT) {
+        $args += "-Dgraphviz.dot.path=$($env:GRAPHVIZ_DOT)"
     }
-}
 
-# Function to check for conflicting environment variables before integration 
tests
-function Test-IntegrationTestEnvVars {
-    $detectedVars = @()
-    $scriptDir = Split-Path -Parent $MyInvocation.PSCommandPath
-    
-    # Check for Elasticsearch environment variables
-    if ($env:UNOMI_ELASTICSEARCH_CLUSTERNAME -or 
-        $env:UNOMI_ELASTICSEARCH_USERNAME -or 
-        $env:UNOMI_ELASTICSEARCH_PASSWORD -or 
-        $env:UNOMI_ELASTICSEARCH_SSL_ENABLE -or 
-        $env:UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES) {
-        $detectedVars += "Elasticsearch"
-    }
-    
-    # Check for OpenSearch environment variables
-    if ($env:UNOMI_OPENSEARCH_CLUSTERNAME -or 
-        $env:UNOMI_OPENSEARCH_ADDRESSES -or 
-        $env:UNOMI_OPENSEARCH_USERNAME -or 
-        $env:UNOMI_OPENSEARCH_PASSWORD -or 
-        $env:UNOMI_OPENSEARCH_SSL_ENABLE -or 
-        $env:UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES) {
-        $detectedVars += "OpenSearch"
-    }
-    
-    if ($detectedVars.Count -gt 0) {
-        Write-Status "error" "Environment variables for $($detectedVars -join 
' and ') are set and will interfere with integration tests"
-        Write-Host ""
-        Write-Host "Integration tests manage their own search engine 
configuration and should not"
-        Write-Host "be run with these environment variables set."
-        Write-Host ""
-        Write-Host "To clear the environment variables, run one of the 
following:"
-        Write-Host ""
-        if ($detectedVars -contains "Elasticsearch") {
-            Write-Host "  . $scriptDir\clear-elasticsearch.sh"
+    $profiles = @()
+    if ($IntegrationTests) {
+        Test-IntegrationTestEnvVars
+        if ($UseOpenSearch) {
+            $args += '-Duse.opensearch=true'
+            $args += '-Popensearch'
+            Write-Host 'Running integration tests with OpenSearch'
+        } else {
+            Write-Host 'Running integration tests with ElasticSearch'
+        }
+        $args += '-Pintegration-tests'
+        if ($SearchHeap) {
+            if ($UseOpenSearch) { $args += "-Dopensearch.heap=$SearchHeap" }
+            else { $args += "-Delasticsearch.heap=$SearchHeap" }
         }
-        if ($detectedVars -contains "OpenSearch") {
-            Write-Host "  . $scriptDir\clear-opensearch.sh"
+        if ($KarafHeap) { $args += "-Dit.karaf.heap=$KarafHeap" }
+        if ($SingleTest) { $args += "-Dit.test=$SingleTest"; Write-Host 
"Running single integration test: $SingleTest" }
+        if ($ItDebug) {
+            $debugOpts = "port=$ItDebugPort"
+            if ($ItDebugSuspend) { $debugOpts += ',hold:true' } else { 
$debugOpts += ',hold:false' }
+            $args += "-Dit.karaf.debug=$debugOpts"
         }
-        Write-Host ""
-        Write-Host "Or manually unset the variables in PowerShell:"
-        if ($detectedVars -contains "Elasticsearch") {
-            Write-Host "  Remove-Item Env:\UNOMI_ELASTICSEARCH_CLUSTERNAME"
-            Write-Host "  Remove-Item Env:\UNOMI_ELASTICSEARCH_USERNAME"
-            Write-Host "  Remove-Item Env:\UNOMI_ELASTICSEARCH_PASSWORD"
-            Write-Host "  Remove-Item Env:\UNOMI_ELASTICSEARCH_SSL_ENABLE"
-            Write-Host "  Remove-Item 
Env:\UNOMI_ELASTICSEARCH_SSL_TRUST_ALL_CERTIFICATES"
+        if ($SkipMigrationTests) { $args += 
'-Dit.test.exclude.pattern=**/migration/**/*IT.java' }
+        if ($KeepContainer) { $args += '-Dit.keepContainer=true' }
+        if ($ResolverDebug) { $args += '-Dit.unomi.resolver.debug=true' }
+        if ($SkipUnitTests) { $args += '-Pskip-unit-tests' }
+    } else {
+        if ($SkipTests) {
+            $profiles += '!integration-tests', '!run-tests'
+            $args += '-DskipTests'
+        } elseif ($SkipUnitTests) {
+            $args += '-Pskip-unit-tests'
         }
-        if ($detectedVars -contains "OpenSearch") {
-            Write-Host "  Remove-Item Env:\UNOMI_OPENSEARCH_CLUSTERNAME"
-            Write-Host "  Remove-Item Env:\UNOMI_OPENSEARCH_ADDRESSES"
-            Write-Host "  Remove-Item Env:\UNOMI_OPENSEARCH_USERNAME"
-            Write-Host "  Remove-Item Env:\UNOMI_OPENSEARCH_PASSWORD"
-            Write-Host "  Remove-Item Env:\UNOMI_OPENSEARCH_SSL_ENABLE"
-            Write-Host "  Remove-Item 
Env:\UNOMI_OPENSEARCH_SSL_TRUST_ALL_CERTIFICATES"
+        if (-not [string]::IsNullOrWhiteSpace($SingleTest)) {
+            Write-Status 'warning' 'SingleTest specified but integration tests 
are not enabled. Use -IntegrationTests.'
         }
-        Write-Host ""
-        Write-Host "After clearing the variables, you can run the integration 
tests again."
-        exit 1
     }
-}
 
-function Get-MavenOptions {
-    $options = @()
-    
-    # Basic options
-    if ($MavenDebug) {
-        $options += "-X"
-    }
-    if ($Offline) {
-        $options += "-o"
+    if ($profiles.Count -gt 0) {
+        $args += "-P$($profiles -join ',')"
     }
-    if ($SkipTests) {
-        $options += "-DskipTests"
-    }
-    if ($IntegrationTests) {
-        # Check for conflicting environment variables before running 
integration tests
-        Test-IntegrationTestEnvVars
-        $options += "-Pintegration-tests"
-    }
-    if ($ResolverDebug) {
-        $options += "-Dit.unomi.resolver.debug=true"
+    return $args
+}
+
+function Invoke-MavenPhase {
+    param(
+        [string]$Phase,
+        [string[]]$ExtraArgs,
+        [switch]$AllowFailure
+    )
+    $mvnArgs = @($Phase) + $ExtraArgs
+    Write-Host "Running: mvn $($mvnArgs -join ' ')"
+    & mvn @mvnArgs
+    $code = $LASTEXITCODE
+    if ($code -ne 0) {
+        Write-Status 'error' "Maven $Phase failed"
+        if ($AllowFailure) { return $code }
+        exit $code
+    }
+    return 0
+}
+
+function Write-ItRunTraceStart {
+    param([string[]]$MvnArgs)
+    $traceDir = Join-Path $ScriptDir 'itests\target'
+    New-Item -ItemType Directory -Force -Path $traceDir | Out-Null
+    $traceFile = Join-Path $traceDir 'it-run-trace.properties'
+    $engine = if ($UseOpenSearch) { 'opensearch' } else { 'elasticsearch' }
+    @(
+        '# IT run trace (written by build.ps1 after clean, before install)'
+        'trace.phase=started'
+        
"trace.started=$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))"
+        "build.invocation=$($script:BuildScriptInvocation -join ' ')"
+        "maven.clean.command=mvn clean $($MvnArgs -join ' ')"
+        "maven.install.command=mvn install $($MvnArgs -join ' ')"
+        "use.opensearch=$UseOpenSearch"
+        "search.engine=$engine"
+        "search.heap=$SearchHeap"
+        "karaf.heap=$KarafHeap"
+        "single.test=$SingleTest"
+        "it.debug=$ItDebug"
+        "it.debug.port=$ItDebugPort"
+        "it.debug.suspend=$ItDebugSuspend"
+        "skip.migration.tests=$SkipMigrationTests"
+        "it.keep.container=$KeepContainer"
+        "it.memory.sampler=$($script:ItMemorySampler)"
+        "it.memory.interval=$MemoryInterval"
+        "maven.debug=$MavenDebug"
+        "maven.offline=$Offline"
+        "maven.quiet=$MavenQuiet"
+        "host=$env:COMPUTERNAME"
+    ) | Set-Content -Path $traceFile -Encoding UTF8
+}
+
+function Start-ItMemorySampler {
+    $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh'
+    if (-not $script:ItMemorySampler -or -not (Test-Path $sampler)) { return }
+    if (-not (Test-CommandExists 'bash')) {
+        Write-Status 'warning' 'bash not found; skipping IT memory sampler'
+        return
+    }
+    & bash $sampler start --target-dir (Join-Path $ScriptDir 'itests\target') 
--interval $MemoryInterval
+    if ($LASTEXITCODE -ne 0) { Write-Status 'warning' 'Could not start IT 
memory sampler' }
+}
+
+function Stop-ItMemorySampler {
+    $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh'
+    if (-not (Test-Path $sampler) -or -not (Test-CommandExists 'bash')) { 
return }
+    & bash $sampler stop --target-dir (Join-Path $ScriptDir 'itests\target') 
2>$null
+}
+
+function Complete-ItRunTrace {
+    param([int]$ExitCode)
+    $traceFile = Join-Path $ScriptDir 'itests\target\it-run-trace.properties'
+    if (-not (Test-Path $traceFile)) { return }
+    Add-Content -Path $traceFile -Value "trace.phase=completed"
+    Add-Content -Path $traceFile -Value 
"trace.completed=$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))"
+    Add-Content -Path $traceFile -Value "maven.exit.code=$ExitCode"
+}
+
+function Write-ItRunOperatorNote {
+    $sampler = Join-Path $ScriptDir 'itests\sample-it-memory.sh'
+    if (-not (Test-Path $sampler) -or -not (Test-CommandExists 'bash')) { 
return }
+    & bash $sampler operator-note --target-dir (Join-Path $ScriptDir 
'itests\target') 2>$null
+}
+
+$script:StartTime = $null
+function Start-BuildTimer { $script:StartTime = Get-Date }
+function Get-BuildElapsed {
+    if (-not $script:StartTime) { return '00:00' }
+    $elapsed = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
+    return '{0:d2}:{1:d2}' -f [math]::Floor($elapsed / 60), ($elapsed % 60)
+}
+
+if ($LogFile) {
+    $logDir = Split-Path -Parent $LogFile
+    if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory 
-Force -Path $logDir | Out-Null }
+    Start-Transcript -Path $LogFile -Append | Out-Null
+}
+
+Write-Section 'Apache Unomi Build Script'
+$mvnArgs = Get-MavenArgumentList
+
+Write-Host @'
+
+     ____  _    _ _____ _      ____
+    |  _ \| |  | |_   _| |    |  _ \
+    | |_) | |  | | | | | |    | | | |
+    |  _ <| |  | | | | | |    | | | |
+    | |_) | |__| |_| |_| |____| |_| |
+    |____/ \____/|_____|______|____/
+
+'@
+Write-Host 'Building...'
+Write-Host 'Estimated time: 3-5 minutes for build, 50-60 minutes with 
integration tests'
+Start-BuildTimer
+
+$totalSteps = if ($Javadoc) { 3 } else { 2 }
+$currentStep = 0
+
+Write-BuildProgress (++$currentStep) $totalSteps 'Cleaning previous build...'
+Write-Host ''
+Invoke-MavenPhase -Phase 'clean' -ExtraArgs $mvnArgs | Out-Null
+
+if ($IntegrationTests) {
+    Write-ItRunTraceStart -MvnArgs $mvnArgs
+    Start-ItMemorySampler
+}
+
+Write-BuildProgress (++$currentStep) $totalSteps 'Compiling and installing 
artifacts...'
+Write-Host ''
+$installExit = Invoke-MavenPhase -Phase 'install' -ExtraArgs $mvnArgs 
-AllowFailure
+if ($IntegrationTests) {
+    Stop-ItMemorySampler
+    Complete-ItRunTrace -ExitCode $installExit
+    Write-ItRunOperatorNote
+}
+if ($installExit -ne 0) { exit $installExit }
+
+Write-Status 'success' "Build completed in $(Get-BuildElapsed)"
+
+if ($Javadoc) {
+    Write-Section 'Javadoc Validation'
+    Write-BuildProgress (++$currentStep) $totalSteps 'Generating and 
validating Javadoc...'
+    Write-Host ''
+    $javadocArgs = @('javadoc:javadoc', '-DskipTests') + $mvnArgs
+    Write-Host "Running: mvn $($javadocArgs -join ' ')"
+    & mvn @javadocArgs
+    if ($LASTEXITCODE -ne 0) {
+        Write-Status 'error' 'Javadoc validation failed — fix doclint errors 
above before pushing'
+        exit $LASTEXITCODE
+    }
+    Write-Status 'success' 'Javadoc validated successfully'
+}
+
+if ($Deploy) {
+    Write-Section 'Deploying to Apache Karaf'
+    $karFile = Join-Path $ScriptDir 
"kar\target\unomi-kar-$($env:UNOMI_VERSION).kar"
+    if (-not (Test-Path $karFile)) {
+        Write-Status 'error' "KAR file not found: $karFile"
+        exit 1
     }
-    if ($NoMavenCache) {
-        $options += "-Dmaven.buildcache.enabled=false"
+    Copy-Item -Force $karFile (Join-Path $KarafHome 'deploy\')
+    Write-Status 'success' 'KAR package copied successfully'
+
+    $repo = Join-Path $KarafHome 'data\maven\repository'
+    if (Test-Path $repo) {
+        Get-ChildItem $repo | Remove-Item -Recurse -Force
+        Write-Status 'success' 'Karaf Maven repository purged'
     }
-    if ($UseOpenSearch) {
-        $options += "-Popensearch"
+    $tmp = Join-Path $KarafHome 'data\tmp'
+    if (Test-Path $tmp) {
+        Get-ChildItem $tmp | Remove-Item -Recurse -Force
+        Write-Status 'success' 'Karaf temporary files purged'
     }
-    
-    return $options -join " "
+    Write-Status 'success' 'Deployment completed'
 }
 
-function Invoke-MavenBuild {
-    Write-Section "Building Apache Unomi"
-    
-    Start-Timer
-    
-    $mvnOptions = Get-MavenOptions
-    $buildCommand = "mvn clean install $mvnOptions"
-    
-    if ($ResolverDebug) {
-        Write-Status "info" "Karaf Resolver debug logging enabled for 
integration tests"
+if (-not $NoKaraf) {
+    $packageTarget = Join-Path $ScriptDir 'package\target'
+    if (-not (Test-Path $packageTarget)) {
+        Write-Status 'error' 'Build directory not found. Did the build 
complete successfully?'
+        exit 1
     }
-    
-    Write-Status "info" "Running: $buildCommand"
-    Write-Host ""
-    
+
+    Push-Location $packageTarget
     try {
-        Invoke-Expression $buildCommand
-        if ($LASTEXITCODE -ne 0) {
-            Write-Status "error" "Maven build failed with exit code 
$LASTEXITCODE"
-            Write-Status "info" "Check the build output above for errors"
-            Write-Status "info" "For more information, see BUILDING.md in the 
project root"
+        $archive = Join-Path $packageTarget 
"unomi-$($env:UNOMI_VERSION).tar.gz"
+        if (-not (Test-Path $archive)) {
+            Write-Status 'error' "Unomi package not found: $archive"
             exit 1
         }
-        Write-Status "success" "Build completed successfully in 
$(Get-ElapsedTime)"
-    } catch {
-        Write-Status "error" "Maven build failed: $_"
-        Write-Status "info" "Check the build output above for errors"
-        Write-Status "info" "For more information, see BUILDING.md in the 
project root"
-        exit 1
-    }
-}
+        & tar -xzf $archive
+        $unomiHome = Join-Path $packageTarget "unomi-$($env:UNOMI_VERSION)"
 
-function Deploy-ToKaraf {
-    param(
-        [string]$KarafPath
-    )
-    
-    Write-Section "Deploying to Apache Karaf"
-    
-    if (-not $KarafPath) {
-        Write-Status "error" "Karaf home directory not specified"
-        Write-Status "info" "Please specify the Karaf home directory using 
-KarafHome"
-        exit 1
-    }
-    
-    if (-not (Test-Path $KarafPath)) {
-        Write-Status "error" "Karaf home directory not found: $KarafPath"
-        Write-Status "info" "Please ensure the directory exists and try again"
-        exit 1
-    }
-    
-    # Stop Karaf if it's running
-    $karafPid = $null
-    if (Test-Path "$KarafPath\data\karaf.pid") {
-        $karafPid = Get-Content "$KarafPath\data\karaf.pid"
-        $process = Get-Process -Id $karafPid -ErrorAction SilentlyContinue
-        if ($process) {
-            Write-Status "info" "Stopping Karaf instance (PID: $karafPid)"
-            Stop-Process -Id $karafPid -Force
-            Start-Sleep -Seconds 5
+        foreach ($pair in @(
+            @{ Src = Join-Path $ScriptDir 'GeoLite2-City.mmdb'; Dest = 
Join-Path $unomiHome 'etc\GeoLite2-City.mmdb' }
+            @{ Src = Join-Path $ScriptDir 'allCountries.zip'; Dest = Join-Path 
$unomiHome 'etc\allCountries.zip' }
+        )) {
+            if (Test-Path $pair.Src) {
+                Copy-Item -Force $pair.Src $pair.Dest
+            }
         }
-    }
-    
-    # Clean Karaf directories
-    Write-Status "info" "Cleaning Karaf directories"
-    Remove-Item -Force -Recurse -ErrorAction SilentlyContinue `
-        "$KarafPath\data",
-        "$KarafPath\cache",
-        "$KarafPath\log"
-    
-    # Copy artifacts
-    Write-Status "info" "Copying artifacts to Karaf deploy directory"
-    Copy-Item -Force -Recurse "package\target\unomi-*\*" $KarafPath
-    
-    # Configure debug options if needed
-    if ($Debug) {
-        $debugOpts = "debug"
-        if ($DebugSuspend) {
-            $debugOpts += "=suspend"
+
+        $karafOptsParts = @()
+        if ($AutoStart) {
+            $karafOptsParts += "-Dunomi.autoStart=$AutoStart"
+            Write-Status 'info' "Configuring auto-start for $AutoStart"
         }
-        $debugOpts += "=$DebugPort"
-        
-        $envContent = @"
-EXTRA_JAVA_OPTS=`"-agentlib:jdwp=transport=dt_socket,server=y,$debugOpts`"
-"@
-        Set-Content -Path "$KarafPath\bin\setenv.bat" -Value $envContent
-    }
-    
-    # Start Karaf
-    if (-not $NoKaraf) {
-        Write-Status "info" "Starting Karaf"
-        $karafCmd = "$KarafPath\bin\karaf.bat"
+        if (-not [string]::IsNullOrWhiteSpace($Distribution)) {
+            $karafOptsParts += "-Dunomi.distribution=$Distribution"
+            Write-Status 'info' "Using Unomi distribution: $Distribution"
+        }
+        if ($karafOptsParts.Count -gt 0) {
+            $env:KARAF_OPTS = $karafOptsParts -join ' '
+        }
+
         if ($Debug) {
-            Write-Status "info" "Debug port: $DebugPort (suspend: 
$DebugSuspend)"
+            if (-not (Test-PortAvailable $DebugPort)) {
+                Write-Status 'error' "Port $DebugPort is already in use"
+                exit 1
+            }
+            $env:KARAF_DEBUG = 'true'
+            $env:JAVA_DEBUG_OPTS = 
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=$($script:KarafDebugSuspend),address=$DebugPort"
+            Write-Status 'info' "Debug mode enabled (port: $DebugPort, 
suspend: $($script:KarafDebugSuspend))"
         }
-        
-        if ($AutoStart) {
-            Write-Status "info" "Auto-starting with $AutoStart"
-            Start-Process $karafCmd -ArgumentList "clean" -NoNewWindow
-        } else {
-            Start-Process $karafCmd -NoNewWindow
+
+        $karafBat = Join-Path $unomiHome 'bin\karaf.bat'
+        if (-not (Test-Path $karafBat)) {
+            Write-Status 'error' 'Karaf executable not found: karaf.bat'
+            exit 1
+        }
+        Push-Location (Join-Path $unomiHome 'bin')
+        try {
+            & $karafBat
+            if ($LASTEXITCODE -ne 0) {
+                Write-Status 'error' 'Karaf failed to start'
+                exit $LASTEXITCODE
+            }
+        } finally {
+            Pop-Location
         }
-        
-        Write-Status "success" "Karaf started successfully"
-        Write-Status "info" "Use bin\client.bat to connect to the console"
-        Write-Status "info" "Default credentials: karaf/karaf"
+    } finally {
+        Pop-Location
+    }
+} else {
+    Write-Status 'info' 'Skipping Karaf startup (-NoKaraf specified)'
+    if ($AutoStart) {
+        Write-Status 'info' "Note: auto-start ($AutoStart) will be applied 
when Karaf is started manually"
     }
 }
 
-# Build and deploy
-Invoke-MavenBuild
+Write-Host @'
 
-if ($Deploy) {
-    Deploy-ToKaraf -KarafPath $KarafHome
-}
+     ____   ___  _   _ _____ _
+    |  _ \ / _ \| \ | | ____| |
+    | | | | | | |  \| |  _| | |
+    | |_| | |_| | |\  | |___|_|
+    |____/ \___/|_| \_|_____(_)
+
+'@
+Write-Host 'Operation completed successfully.'
 
-Write-Status "success" "Build process completed successfully!" 
\ No newline at end of file
+if ($LogFile) {
+    Stop-Transcript | Out-Null
+}

Reply via email to