=== modified file 'bootstrap_conversion.cmd'
--- old/bootstrap_conversion.cmd	2025-04-29 05:00:02 +0000
+++ new/bootstrap_conversion.cmd	2026-08-07 14:04:48 +0000
@@ -4,5 +4,6 @@
 :: Determine script directory
 set scriptdir=%~dp0
 
-:: Pass all arguments to PowerShell script
-pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%bootstrap_conversion.ps1" %*
+:: Pass all arguments to PowerShell script, run_powershell.cmd picks the PowerShell which is installed
+call "%scriptdir%run_powershell.cmd" "%scriptdir%bootstrap_conversion.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'build.xml'
--- old/build.xml	2026-06-23 18:00:55 +0000
+++ new/build.xml	2026-08-07 14:12:06 +0000
@@ -110,6 +110,23 @@
       <os family="unix" />
    </condition>
 
+   <!-- The PowerShell to run the .ps1 scripts with: PowerShell Core when it is installed, otherwise the
+        Windows PowerShell which is preinstalled with Windows.  A .cmd wrapper is not used here, ant
+        cannot exec a batch file (CreateProcess only runs a real executable), which is also why the .ps1
+        scripts are passed to the interpreter instead of being exec'd directly.  Should neither spelling
+        of the variable resolve, this falls back to powershell, the one always present on Windows.
+
+        Both env.PATH and env.Path are looked at: ant names these properties after the environment
+        variables as the OS reports them, and Windows spells that one "Path".  Checking only the
+        uppercase name would silently build on the Windows PowerShell on a machine which does have
+        PowerShell Core installed. -->
+   <property environment="env" />
+   <available file="pwsh.exe" filepath="${env.PATH}" property="havePowerShellCore" />
+   <available file="pwsh.exe" filepath="${env.Path}" property="havePowerShellCore" />
+   <condition property="ps.executable" value="pwsh" else="powershell">
+      <isset property="havePowerShellCore" />
+   </condition>
+
    <condition property="escaped.quotes" value="&quot;&quot;&quot;" else="&quot;">
       <isset property="isWindows"  />
    </condition>
@@ -139,17 +156,29 @@
 
    <!-- path used when running application related tasks -->
    <path id="app.classpath">
-      <fileset dir="${fwd.lib.home}"    includes="*.jar"/>
-      <fileset dir="${deploy.home}/lib" includes="*.jar"/>
+      <pathelement location="${fwd.lib.home}/p2j.jar"/>
+      <pathelement location="${fwd.lib.home}/fwdspi.jar"/>
+      <pathelement location="${fwd.lib.home}/fwdaopltw.jar"/>
+      <pathelement location="${deploy.home}/lib/${appname}.jar"/>
    </path>
 
    <!-- path used when running database import tasks -->
    <path id="import.classpath">
-      <fileset dir="${fwd.lib.home}"    includes="*.jar"/>
-      <fileset dir="${deploy.home}/lib" includes="*.jar"/>
-      <fileset dir="${build.home}/lib"  includes="*.jar"/>
+      <pathelement location="${fwd.lib.home}/p2j.jar"/>
+      <pathelement location="${fwd.lib.home}/fwdspi.jar"/>
+      <pathelement location="${fwd.lib.home}/fwdaopltw.jar"/>
+      <pathelement location="${deploy.home}/lib/${appname}.jar"/>
+      <pathelement location="${build.home}/lib/${appname}.jar"/>
    </path>
-   
+
+   <target name="show.classpath" description="Print the classpaths used by the java tasks.">
+      <echo message="fwd.lib.home      = ${fwd.lib.home}"/>
+      <echo message="deploy lib        = ${deploy.home}/lib"/>
+      <echo message="compile.classpath = ${toString:compile.classpath}"/>
+      <echo message="app.classpath     = ${toString:app.classpath}"/>
+      <echo message="import.classpath  = ${toString:import.classpath}"/>
+   </target>
+
    <tstamp>
       <format property="DSTAMP" pattern="yyyyMMdd" />
    </tstamp>
@@ -172,9 +201,13 @@
    <target name="init-standard-df" depends="init-ant-contrib"
            description="Determine correct standard.df based on p2j build">
 
-      <!-- Define script path -->
+      <!-- Define script path. Windows cannot execute the bash script, it uses the .ps1 counterpart. -->
+      <condition property="standard.selector.name"
+                 value="select_standard.ps1" else="select_standard.sh">
+         <isset property="isWindows"/>
+      </condition>
       <property name="standard.selector.script"
-                location="${data.rel}/select_standard.sh"/>
+                location="${data.rel}/${standard.selector.name}"/>
 
       <!-- Check if script exists -->
       <available file="${standard.selector.script}"
@@ -187,7 +220,22 @@
             <echo message="Using selector script: ${standard.selector.script}"/>
             <exec executable="${standard.selector.script}"
                   outputproperty="standard.selector.result"
-                  failonerror="true">
+                  failonerror="true"
+                  osfamily="unix">
+                <arg value="${data.rel}"/>
+                <arg value="${fwd.lib.home}/p2j.jar"/>
+            </exec>
+
+            <!-- CreateProcess cannot run a .ps1 either, the interpreter is launched explicitly -->
+            <exec executable="${ps.executable}"
+                  outputproperty="standard.selector.result"
+                  failonerror="true"
+                  osfamily="windows">
+                <arg value="-NoProfile"/>
+                <arg value="-ExecutionPolicy"/>
+                <arg value="Bypass"/>
+                <arg value="-File"/>
+                <arg value="${standard.selector.script}"/>
                 <arg value="${data.rel}"/>
                 <arg value="${fwd.lib.home}/p2j.jar"/>
             </exec>
@@ -209,13 +257,14 @@
       <condition property="standard.source"
                  value="${data.rel}/standard_post9950.df"
                  else="${data.rel}/standard_pre9950.df">
-          <equals arg1="${standard.selector.result}" arg2="post9950"/>
+          <!-- trim, the PowerShell selector terminates its output with a CRLF -->
+          <equals arg1="${standard.selector.result}" arg2="post9950" trim="true"/>
       </condition>
 
       <!-- Perform copy only if not skip -->
       <if>
          <not>
-            <equals arg1="${standard.selector.result}" arg2="skip"/>
+            <equals arg1="${standard.selector.result}" arg2="skip" trim="true"/>
          </not>
          <then>
             <echo message="Copying ${standard.source} to ${data.rel}/standard.df"/>
@@ -633,7 +682,7 @@
          <arg value="deploy_appcds.sh"/>
          <arg value="${deploy.home}/lib/p2j.jar"/>
       </exec>
-      <exec executable="pwsh" dir="${deploy.home}/server" failonerror="true" osfamily="windows">
+      <exec executable="${ps.executable}" dir="${deploy.home}/server" failonerror="true" osfamily="windows">
          <arg value="-ExecutionPolicy"/>
          <arg value="Bypass"/>
          <arg value="-File"/>
@@ -697,7 +746,7 @@
          <arg value="-no"/>
       </exec>
 
-      <exec executable="pwsh" dir="." failonerror="true" osfamily="windows">
+      <exec executable="${ps.executable}" dir="." failonerror="true" osfamily="windows">
          <arg value="-ExecutionPolicy"/>
          <arg value="Bypass"/>
          <arg value="-File"/>
@@ -987,7 +1036,7 @@
          <arg value="find ${app.4gl.src} -type f -name '*.w' ${e4gl-noclean} -exec grep -l ${e4gl.pattern} {} \; -exec rm -f {} +" />
       </exec>
 
-      <exec executable="powershell" failonerror="true" osfamily="windows">
+      <exec executable="${ps.executable}" failonerror="true" osfamily="windows">
          <arg value="-Command"/>
          <arg value="Get-ChildItem -Path '${app.4gl.src}' -Filter '*.w' -Recurse | Select-String -Pattern ${e4gl-ps1.pattern} | ForEach-Object { Remove-Item $_.Path -Force }"/>
       </exec>

=== modified file 'build_db.xml'
--- old/build_db.xml	2026-02-24 22:54:08 +0000
+++ new/build_db.xml	2026-08-07 14:04:48 +0000
@@ -14,6 +14,13 @@
    <condition property="java.locale.providers.param" else="-Djava.locale.providers=SPI,JRE" value="-Djava.locale.providers=SPI,CLDR,COMPAT">
        <javaversion atleast="11"/>
    </condition>
+   <!-- Run with -Dclasspath.debug=true to have the forked JVMs report every class they load together
+        with the jar it came from.  The output is captured by the <record> log of the target.  This is
+        the only way to see the jars which the Class-Path manifest of p2j.jar contributes, those are
+        resolved by the JVM and never appear in the classpath itself. -->
+   <condition property="classpath.log.param" value="-verbose:class" else="-Dignorethis">
+      <isset property="classpath.debug"/>
+   </condition>
    <if>
       <and>
          <isset property="db.sql.shared" />
@@ -47,6 +54,7 @@
            description="Creates an empty H2 database instance using the DDL generated during conversion."
            if="${db.h2}">
       <record name="create_db_h2_${db.name}_${LOG_STAMP}.log" action="start"/>
+      <echo message="app.classpath = ${toString:app.classpath}"/>
       <java classname="com.goldencode.p2j.persist.deploy.ScriptRunner"
             fork="true"
             failonerror="true"
@@ -54,6 +62,7 @@
          <jvmarg value="-Xmx1g"/>
          <jvmarg value="${java.locale.providers.param}"/>
          <jvmarg value="${java.ext.dir.param}"/>
+         <jvmarg value="${classpath.log.param}"/>
          <jvmarg value="-Dfile.encoding=UTF-8"/>
          <jvmarg value="-Djava.util.logging.config.file=${fwd.home}/cfg/logging.properties"/>
          <arg value ="${sql.url.h2}"/>
@@ -72,6 +81,7 @@
            description="Import data (.d files) into H2 database."
            if="${db.h2}">
       <record name="import_db_h2_${db.name}_${LOG_STAMP}.log" action="start"/>
+      <echo message="import.classpath = ${toString:import.classpath}"/>
       <java classname="com.goldencode.p2j.pattern.PatternEngine"
             fork="true"
             failonerror="true"
@@ -79,6 +89,7 @@
          <jvmarg value="-Xmx1g"/>
          <jvmarg value="${java.locale.providers.param}"/>
          <jvmarg value="${java.ext.dir.param}"/>
+         <jvmarg value="${classpath.log.param}"/>
          <jvmarg value="-Dfile.encoding=UTF-8"/>
          <jvmarg value="-Djava.system.class.loader=com.goldencode.asm.AsmClassLoader"/>
          <arg value ="-d"/>

=== added file 'data/select_standard.ps1'
--- old/data/select_standard.ps1	1970-01-01 00:00:00 +0000
+++ new/data/select_standard.ps1	2026-08-07 14:04:48 +0000
@@ -0,0 +1,78 @@
+#
+# The Windows counterpart of select_standard.sh, kept in sync with it: Windows cannot execute the bash
+# script, and the policy rules below decide which standard.df the build uses, so the two MUST agree.
+#
+param (
+   [Parameter(Position = 0, Mandatory = $true)] [string]$DataRel,
+   [Parameter(Position = 1, Mandatory = $true)] [string]$JarPath
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+# Get version line.  Only stdout is captured, exactly as the "$(java -jar "$jar_path")" of
+# select_standard.sh does; whatever java puts on stderr is left to flow into the build log.
+#
+# The preference is relaxed for the call: Windows PowerShell turns the stderr of a native command
+# into an error record, which the $ErrorActionPreference = 'Stop' above would then make terminating,
+# and a JVM which merely reports "Picked up JAVA_TOOL_OPTIONS" would fail the build.  PowerShell 7
+# does not do this.
+$previousPreference = $ErrorActionPreference
+$ErrorActionPreference = 'Continue'
+try {
+   $output = (& java -jar $JarPath | Out-String).Trim()
+   $javaExit = $LASTEXITCODE
+}
+finally {
+   $ErrorActionPreference = $previousPreference
+}
+
+# the counterpart of the "set -e" of select_standard.sh, which aborts on a failing java
+if ($javaExit -ne 0)
+{
+   [Console]::Error.WriteLine("ERROR: java -jar $JarPath failed with exit code $javaExit")
+   exit 1
+}
+
+# Expect format: FWD v4.0.0_p2j_<branch>_<revision>
+if ($output -notmatch '_p2j_([^_]+)_([0-9]+)')
+{
+   [Console]::Error.WriteLine("ERROR: Unable to parse p2j output: $output")
+   exit 1
+}
+
+$branch   = $Matches[1]
+$revision = [int]$Matches[2]
+
+# Default to using post9950 update. Add policies for using older standard.df
+$usePre = $false
+
+# ---- Policy Rules ----
+if ($branch -eq "trunk" -and $revision -lt 16415)
+{
+   $usePre = $true
+}
+
+# Policy example. Any of these should exist only until the branch is rebased to trunk_16415
+
+#if ($branch -eq "9986c")
+#{
+#   $usePre = $true
+#}
+# ----------------------
+
+if ($usePre)
+{
+   Write-Output "pre9950"
+   exit 0
+}
+
+# Fallback handling uses the in-place standard.df (post9950). Tell ant 'skip'
+if (!(Test-Path -LiteralPath (Join-Path $DataRel 'standard_pre9950.df') -PathType Leaf) -and
+     (Test-Path -LiteralPath (Join-Path $DataRel 'standard.df') -PathType Leaf))
+{
+   Write-Output "skip"
+   exit 0
+}
+
+Write-Output "post9950"

=== modified file 'data/select_standard.sh'
--- old/data/select_standard.sh	2026-02-16 14:49:33 +0000
+++ new/data/select_standard.sh	2026-08-07 14:04:48 +0000
@@ -21,7 +21,7 @@
 
 # ---- Policy Rules ----
 if [[ "$branch" == "trunk" && "$revision" -lt 16415 ]]; then
-   use_post=false
+   use_pre=true
 fi
 
 # Policy example. Any of these should exist only until the branch is rebased to trunk_16415

=== modified file 'deploy/client/client.cmd'
--- old/deploy/client/client.cmd	2025-04-29 05:00:02 +0000
+++ new/deploy/client/client.cmd	2026-08-07 14:04:48 +0000
@@ -1,5 +1,13 @@
 :: Standard FWD client startup script - Wrapper to client.ps1
 @echo off
 setlocal
-pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0client.ps1" %*
-endlocal
+
+:: PowerShell Core when it is installed, otherwise the Windows PowerShell which is preinstalled with
+:: Windows.  %%~$PATH:I expands to the full path of the file when it is found on the PATH, and to an
+:: empty string when it is not.
+set "psexe="
+for %%I in (pwsh.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+if not defined psexe set "psexe=powershell.exe"
+
+"%psexe%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0client.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'deploy/client/client.ps1'
--- old/deploy/client/client.ps1	2025-04-29 05:46:04 +0000
+++ new/deploy/client/client.ps1	2026-08-07 14:04:48 +0000
@@ -148,7 +148,8 @@
 }
 
 if ($debug) {
-   $daddress = Test-Path "/.dockerenv" ? "0.0.0.0:"+$dport : $dport
+   # an if expression, the ?: ternary operator needs PowerShell 7
+   $daddress = if (Test-Path "/.dockerenv") { "0.0.0.0:" + $dport } else { $dport }
    $dtxt = "-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address="+$daddress+",server=y,suspend="+$suspend
 } else {
    $dtxt = ""

=== modified file 'deploy/deploy_helpers.ps1'
--- old/deploy/deploy_helpers.ps1	2025-04-29 05:00:02 +0000
+++ new/deploy/deploy_helpers.ps1	2026-08-07 14:04:48 +0000
@@ -7,14 +7,29 @@
       [string]$JavaExecutable = "java"  # default to 'java' unless overridden
    )
 
-   $output = & $JavaExecutable -version 2>&1 |
+   # java writes its version banner to stderr, and Windows PowerShell turns the stderr of a native
+   # command into an error record, which the $ErrorActionPreference = 'Stop' of the callers then makes
+   # terminating ("NativeCommandError").  PowerShell 7 does not do this.  Relax the preference for the
+   # call only, and stringify what comes back: with the streams merged the lines can arrive as error
+   # records rather than as strings.
+   $previousPreference = $ErrorActionPreference
+   $ErrorActionPreference = 'Continue'
+   try {
+      $versionLines = & $JavaExecutable -version 2>&1 | ForEach-Object { "$_" }
+   }
+   finally {
+      $ErrorActionPreference = $previousPreference
+   }
+
+   $output = $versionLines |
       Where-Object { $_ -match 'version' } |
       ForEach-Object {
          if ($_ -match '"([\d._]+)"') {
             $version = $matches[1]
             if ($version -like '1.*') {
                $parts = $version -split '\.'
-               return [int]("${parts[0]}${parts[1]}")
+               # "${parts[0]}" does not index, it names a variable called 'parts[0]'
+               return [int]("$($parts[0])$($parts[1])")
             } else {
                return [int]($version -split '[._]')[0] * 10
             }

=== modified file 'deploy/server/deploy_appcds.ps1'
--- old/deploy/server/deploy_appcds.ps1	2026-07-17 09:38:55 +0000
+++ new/deploy/server/deploy_appcds.ps1	2026-08-07 14:04:48 +0000
@@ -122,8 +122,8 @@
         continue
     }
 
-    Write-Host "JVM classpath for $clientType: $clientCp"
-    Write-Host "JVM args for $clientType: $clientArgsStr"
+    Write-Host "JVM classpath for ${clientType}: $clientCp"
+    Write-Host "JVM args for ${clientType}: $clientArgsStr"
 
     # The archive is dumped below with our own -Xshare:dump and a fresh
     # -XX:SharedArchiveFile pointing at the .jsa we're generating. Strip the

=== modified file 'deploy/server/prepare_dir.ps1'
--- old/deploy/server/prepare_dir.ps1	2026-05-21 12:19:57 +0000
+++ new/deploy/server/prepare_dir.ps1	2026-08-07 14:04:48 +0000
@@ -1,168 +1,326 @@
-# prepare_dir.ps1
-
-# Parse parameters
-param (
-   [switch]$o,
-   [string]$f
-)
-
-Set-PSDebug -Trace 0
-Set-StrictMode -Version Latest
-$ErrorActionPreference = 'Stop'
-
-# Ensure the script is running with administrative privileges
-if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
-{
-   Write-Host "This script requires administrative privileges. Restarting with elevation..."
-   Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" -Verb RunAs
-   exit
-}
-
-# Helper Functions
-function Show-Usage {
-   Write-Host "Usage: prepare_dir.ps1 [-o] [-f <json_cfg_file>]"
-}
-
-function Get-Val($Key, $Default, $File) {
-   if (Test-Path $File) {
-      $json = Get-Content $File | ConvertFrom-Json
-      $val = $json | Select-Object -ExpandProperty $Key -ErrorAction SilentlyContinue
-      return $(if ($null -eq $val) { $Default } else { $val })
-   }
-   $Default
-}
-
-function Split-String($InputString) { return $InputString -split ',' }
-
-# Defaults
-$infile = "prepare_dir.json"
-$keyboard = "US"
-$pljava = "FALSE"
-$def_p2j_entry = "start.p"
-$def_propath = ".;common;"
-$def_search_path = ".;common;"
-$defdbname = "hotel"
-
-if ($args -contains "-?" -or $args -contains "/?")
-{
-   Show-Usage
-   exit 1
-}
-
-if ($f) { $infile = $f }
-if (!(Test-Path $infile)) { Write-Error "Input file not found: $infile"; Show-Usage; exit 1 }
-
-# Read JSON values
-$cfg = @{
-   spawner_path     = Get-Val "spawner_path" "/opt/spawner/spawn" $infile
-   server_xml       = Get-Val "server_xml_file" "server.xml" $infile
-   directory_xml    = Get-Val "directory_xml_file" "directory.xml" $infile
-   client_start     = Get-Val "client_start_dir" "./deploy/client" $infile
-   dateFormat       = Get-Val "dateFormat" "mdy" $infile
-   numberGroupSep   = Get-Val "numberGroupSep" "," $infile
-   numberDecimalSep = Get-Val "numberDecimalSep" "." $infile
-   p2j_entry        = Get-Val "p2j_entry" $def_p2j_entry $infile
-   pkgroot          = Get-Val "pkgroot" "com.goldencode.hotel" $infile
-   propath          = Get-Val "propath" $def_propath $infile
-   search_path      = Get-Val "search_path" $def_search_path $infile
-   path_separator   = Get-Val "path_separator" ([IO.Path]::PathSeparator) $infile
-   file_separator   = Get-Val "file_separator" ([IO.Path]::DirectorySeparatorChar) $infile
-   case_sensitive   = Get-Val "case_sensitive" "TRUE" $infile
-   os_user          = Get-Val "os_user" $env:USERNAME $infile
-   kbd_layout       = Get-Val "kbd_layout" $keyboard $infile
-   server_log       = Get-Val "server_log" "../logs" $infile
-   client_log       = Get-Val "client_log" "../logs" $infile
-   embedded_host    = Get-Val "embedded_host" "localhost" $infile
-   admin_port       = Get-Val "admin_port" "7443" $infile
-   dbnames          = Get-Val "dbnames" $defdbname $infile
-   client_lib_path  = Get-Val "client_lib_path" "..\lib" $infile
-}
-
-$db_array = @(Split-String $cfg.dbnames)
-$defdatabase = $db_array[0]
-
-# Determine if we can now create the directory file
-if (Test-Path -LiteralPath $cfg.directory_xml -PathType Leaf) {
-    $dirfile = (Get-Item -LiteralPath $cfg.directory_xml).FullName
-}
-else {
-    $parent = if ([string]::IsNullOrWhiteSpace((Split-Path $cfg.directory_xml -Parent))) {
-        $PWD.Path  # Use current directory if no parent in path
-    } else {
-        (Get-Item (Split-Path $cfg.directory_xml -Parent)).FullName
-    }
-    $dirfile = Join-Path $parent (Split-Path $cfg.directory_xml -Leaf)
-}
-if ((Test-Path $dirfile) -and -not $o.IsPresent) {
-   Write-Error "Output file exists: $dirfile, and '-o' not given."
-   Show-Usage
-   exit 1
-}
-
-# Templates processing
-(Get-Content "server.xml.template") -replace '{directory_xml_file}', $cfg.directory_xml | Set-Content $cfg.server_xml
-
-$directoryTemplate = Get-Content "directory.xml.template"
-foreach ($k in $cfg.Keys) {
-   $directoryTemplate = $directoryTemplate -replace "\{$k\}", $cfg[$k]
-}
-$directoryTemplate = $directoryTemplate -replace '{dbname}', $defdatabase
-$directoryTemplate | Set-Content "directory_tmp.xml"
-
-# DB info and Java merge
-$dbdialect = @{
-   h2       = "com.goldencode.p2j.persist.dialect.P2JH2Dialect"
-   postgres = "com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect"
-}
-$dbdriver = @{
-   h2       = "org.h2.Driver"
-   postgres = "org.postgresql.Driver"
-}
-
-$fwd_lib = $env:FWD_LIB
-if (-not $fwd_lib) { $fwd_lib = "../../p2j" }
-$p2j_jar = if (Test-Path "$fwd_lib/build/lib/p2j.jar") {
-   "$fwd_lib/build/lib/p2j.jar"
-} elseif (Test-Path "$fwd_lib/lib/p2j.jar") {
-   "$fwd_lib/lib/p2j.jar"
-} else {
-   "../../p2j/lib/p2j.jar"
-}
-
-foreach ($db in $db_array) {
-   $dbtype = Get-Val "$db.dbtype" "h2" $infile
-   if ($dbtype -ne "none") {
-      $jdbc_url = if ($dbtype -eq "h2") {
-         $pljava = "TRUE"
-         "h2:$(Get-Val "$db.dbpath" "../db" $infile)/$db;DB_CLOSE_DELAY=-1;MV_STORE=FALSE;RTRIM=TRUE"
-      } else {
-         "postgresql://$(Get-Val "$db.dbhost" "localhost" $infile):$(Get-Val "$db.dbport" "5432" $infile)/$db"
-      }
-
-      $dbfile = "directory_db.xml.$db"
-      (Get-Content "directory_db.xml.template") `
-         -replace '{dbhost}', (Get-Val "$db.dbhost" "localhost" $infile) `
-         -replace '{dbport}', (Get-Val "$db.dbport" "5432" $infile) `
-         -replace '{dbname}', $db `
-         -replace '{dbuser}', (Get-Val "$db.dbuser" "fwd_user" $infile) `
-         -replace '{dbuserpass}', (Get-Val "$db.dbuserpass" "user" $infile) `
-         -replace '{dbadmin}', (Get-Val "$db.dbadmin" "fwd_admin" $infile) `
-         -replace '{dbadminpass}', (Get-Val "$db.dbadminpass" "admin" $infile) `
-         -replace '{dbdialect}', $dbdialect[$dbtype] `
-         -replace '{dbdriver}', $dbdriver[$dbtype] `
-         -replace '{jdbc_url}', $jdbc_url `
-         -replace '{pljava}', $pljava `
-         -replace '{collation}', (Get-Val "$db.collation" "en_US@iso88591_fwd_basic" $infile) |
-         Set-Content $dbfile
-
-      # Java DirectoryCopy
-      java -Xmx256m -classpath $p2j_jar com.goldencode.p2j.directory.DirectoryCopy copy $dbfile /server/standard/database/ directory_tmp.xml /server/standard/database/
-      Remove-Item $dbfile
-   } else {
-      Copy-Item "directory_nodb.xml.template" "directory_nodb.xml"
-   }
-}
-
-Move-Item -Force "directory_tmp.xml" $dirfile
-
-exit 0
+#
+# prepare_dir.ps1 - the Windows counterpart of prepare_dir.sh.
+#
+# Renders server.xml and directory.xml on the deployment machine from prepare_dir.json, which
+# json_template.ps1 -d (or json_template.sh -d) produces at build time.
+#
+# Run this from the directory holding the templates, deploy/server, exactly as prepare_dir.sh is.
+#
+param (
+   [switch]$o,
+   [string]$f
+)
+
+Set-PSDebug -Trace 0
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Show-Usage
+{
+   Write-Host "Usage: prepare_dir.ps1 [-o] [-f <json_cfg_file>]"
+   Write-Host "Where:"
+   Write-Host "   o = Overwrite the specified directory.xml, if it already exists."
+   Write-Host "   f = Use <json_cfg_file> instead of prepare_dir.json as input."
+}
+
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+# Property lookup which tolerates a missing key under Set-StrictMode; $cfg.$Key would throw.
+function Get-Node
+{
+   param (
+      $Object,
+      [string]$Key
+   )
+
+   if ($null -eq $Object) { return $null }
+
+   $prop = $Object.PSObject.Properties[$Key]
+   if ($null -eq $prop) { return $null }
+
+   return $prop.Value
+}
+
+function Get-Val
+{
+   param (
+      [string]$Key,
+      [string]$Default = ""
+   )
+
+   $val = Get-Node $script:cfg $Key
+   if ($null -eq $val -or "$val" -eq "") { return $Default }
+
+   return "$val"
+}
+
+# The per-database values live in a sub-object named after the database, so the lookup is two
+# levels.  This used to pass "<db>.<key>" as a single property name to Select-Object, which never
+# resolves against nested json and silently handed back every default: with
+# prepare_dir_h2_docker.json that turned dbpath into "../db" instead of "/opt/hotel/db", and did the
+# same to the collation, the users and their passwords.
+function Get-SubVal
+{
+   param (
+      [string]$SubName,
+      [string]$Key,
+      [string]$Default = ""
+   )
+
+   $node = Get-Node $script:cfg $SubName
+   $val = Get-Node $node $Key
+   if ($null -eq $val -or "$val" -eq "") { return $Default }
+
+   return "$val"
+}
+
+# Replaces every {key} of a template with its value and writes the result.  A literal String.Replace
+# and not the -replace operator: -replace is a regular expression on both sides, so a value holding
+# a $ or a \ would be mangled - a database password is the obvious way to hit that.
+function Expand-Template
+{
+   param (
+      [string]$Template,
+      [hashtable]$Values,
+      [string]$Destination
+   )
+
+   if (!(Test-Path -LiteralPath $Template -PathType Leaf))
+   {
+      throw "Could not create $Destination, the template $Template was not found."
+   }
+
+   $content = [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $Template).Path)
+
+   foreach ($key in $Values.Keys)
+   {
+      $content = $content.Replace("{$key}", [string]$Values[$key])
+   }
+
+   [System.IO.File]::WriteAllText($Destination, $content, $utf8NoBom)
+}
+
+if ($args -contains "-?" -or $args -contains "/?" -or $args -contains "-h")
+{
+   Show-Usage
+   exit 1
+}
+
+$infile = if ($f) { $f } else { "prepare_dir.json" }
+if (!(Test-Path -LiteralPath $infile -PathType Leaf))
+{
+   Write-Error "Input file not found: $infile"
+   Show-Usage
+   exit 1
+}
+
+$here = (Get-Location).Path
+
+# System.IO and every native command resolve a relative path against the PROCESS working
+# directory, which PowerShell's own location does not track; keep the two in step.
+[Environment]::CurrentDirectory = $here
+$cfg = [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $infile).Path) | ConvertFrom-Json
+
+# Nothing below carries an application specific default any more: pkgroot used to fall back to
+# "com.goldencode.hotel" and dbnames to "hotel", so a deployment whose json was missing either one
+# was configured for the sample application instead of failing.
+$pkgroot = Get-Val "pkgroot"
+$dbnames = Get-Val "dbnames"
+if ([string]::IsNullOrEmpty($pkgroot))
+{
+   Write-Error "No 'pkgroot' in $infile."
+   exit 1
+}
+if ([string]::IsNullOrEmpty($dbnames))
+{
+   Write-Error "No 'dbnames' in $infile."
+   exit 1
+}
+
+# dbnames is a comma separated string in prepare_template.json but a real json array in the
+# prepare_dir_*_docker.json files; accept either.  Splitting an array on ',' joined its elements
+# with a space and yielded a single bogus name.
+$dbnamesNode = Get-Node $cfg "dbnames"
+if ($dbnamesNode -is [System.Array])
+{
+   $db_array = @($dbnamesNode | ForEach-Object { "$_".Trim() } | Where-Object { $_ -ne "" })
+}
+else
+{
+   $db_array = @($dbnames -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" })
+}
+$defdatabase = $db_array[0]
+
+$directory_xml = Get-Val "directory_xml_file" "directory.xml"
+$server_xml = Get-Val "server_xml_file" "server.xml"
+
+# The spawner default follows this host, it used to be the Linux "/opt/spawner/spawn".
+$spawner_path = Get-Val "spawner_path" (Join-Path $env:ProgramData 'FWD\spawner\spawn.exe')
+$client_start_dir = Get-Val "client_start_dir" (Join-Path (Split-Path $here -Parent) 'client')
+
+$client_lib_path = Get-SubVal "appcds" "client_lib_path" "..\lib"
+$webclient_appcds_archive = Get-SubVal "appcds" "webclient_appcds_archive" ""
+$webclient_appcds_jvmargs = (Get-SubVal "appcds" "webclient_appcds_jvmargs" "").Replace(
+                               "{archive}", $webclient_appcds_archive)
+$webclient_memory = Get-Val "webclient_memory" "128m"
+
+# The libPath node is emitted only when an AppCDS block is configured, as json_template does.
+if ($null -ne (Get-Node $cfg "appcds"))
+{
+   $libpath_rendered = @"
+          <node class="string" name="libPath">
+            <node-attribute name="value" value="$client_lib_path"/>
+          </node>
+"@
+}
+else
+{
+   $libpath_rendered = ""
+}
+
+# Where the directory.xml ends up; a bare file name has no parent, which Join-Path rejects.
+$dirParent = Split-Path -Path $directory_xml -Parent
+if ([string]::IsNullOrWhiteSpace($dirParent)) { $dirParent = "." }
+if (!(Test-Path -LiteralPath $dirParent -PathType Container))
+{
+   Write-Error "The directory holding $directory_xml does not exist: $dirParent"
+   exit 1
+}
+$dirfile = Join-Path (Resolve-Path -LiteralPath $dirParent).Path (Split-Path -Path $directory_xml -Leaf)
+
+if ((Test-Path -LiteralPath $dirfile) -and -not $o.IsPresent)
+{
+   Write-Error "Output file exists: $dirfile, and '-o' not given."
+   Show-Usage
+   exit 1
+}
+
+# prepare_dir.sh reaches for sudo here, and only here, when the destination is not writable.  There
+# is no in-place equivalent on Windows, and self-elevating the whole script would restart it in
+# system32 where none of the relative template paths below resolve, so the condition is reported
+# instead and the operator re-runs from an elevated prompt.
+$probe = Join-Path (Split-Path $dirfile -Parent) ".prepare_dir.probe"
+try
+{
+   [System.IO.File]::WriteAllText($probe, "")
+   Remove-Item -LiteralPath $probe -Force
+}
+catch
+{
+   Write-Error ("Cannot write to $(Split-Path $dirfile -Parent). Re-run this script from an " +
+                "elevated prompt, or grant yourself write access to that directory.")
+   exit 1
+}
+
+# ---------------------------------------------------------------------------------------------------
+# server.xml and directory.xml
+# ---------------------------------------------------------------------------------------------------
+Expand-Template 'server.xml.template' @{ directory_xml_file = $directory_xml } $server_xml
+
+# Every marker of directory.xml.template is listed. client_start_dir, libPath,
+# webclient_appcds_jvmargs and webclient_memory used to be absent, so they survived into the
+# generated directory.xml verbatim. admin_console_pw is deliberately left alone, json_template does
+# not fill it either.
+Expand-Template 'directory.xml.template' @{
+   spawner_path             = $spawner_path
+   client_start_dir         = $client_start_dir
+   libPath                  = $libpath_rendered
+   dateFormat               = (Get-Val "dateFormat" "mdy")
+   numberGroupSep           = (Get-Val "numberGroupSep" ",")
+   numberDecimalSep         = (Get-Val "numberDecimalSep" ".")
+   p2j_entry                = (Get-Val "p2j_entry" "start.p")
+   pkgroot                  = $pkgroot
+   propath                  = (Get-Val "propath" ".")
+   search_path              = (Get-Val "search_path" ".")
+   path_separator           = (Get-Val "path_separator" ";")
+   file_separator           = (Get-Val "file_separator" "\")
+   case_sensitive           = (Get-Val "case_sensitive" "FALSE")
+   os_user                  = (Get-Val "os_user" $env:USERNAME)
+   kbd_layout               = (Get-Val "kbd_layout" "US")
+   dbname                   = $defdatabase
+   server_log               = (Get-Val "server_log" "../logs")
+   client_log               = (Get-Val "client_log" "../logs")
+   embedded_host            = (Get-Val "embedded_host" "localhost")
+   admin_port               = (Get-Val "admin_port" "7443")
+   webclient_appcds_jvmargs = $webclient_appcds_jvmargs
+   webclient_memory         = $webclient_memory
+} 'directory_tmp.xml'
+
+# ---------------------------------------------------------------------------------------------------
+# Merge each database into the directory
+# ---------------------------------------------------------------------------------------------------
+$dbdialect = @{
+   h2       = "com.goldencode.p2j.persist.dialect.P2JH2Dialect"
+   postgres = "com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect"
+}
+$dbdriver = @{
+   h2       = "org.h2.Driver"
+   postgres = "org.postgresql.Driver"
+}
+
+$fwd_lib = if ($env:FWD_LIB) { $env:FWD_LIB } else { "../../p2j" }
+$p2j_jar = if (Test-Path -LiteralPath "$fwd_lib/build/lib/p2j.jar") { "$fwd_lib/build/lib/p2j.jar" }
+           elseif (Test-Path -LiteralPath "$fwd_lib/lib/p2j.jar")   { "$fwd_lib/lib/p2j.jar" }
+           else                                                     { "../../p2j/lib/p2j.jar" }
+
+foreach ($db in $db_array)
+{
+   $dbtype = Get-SubVal $db "dbtype" "h2"
+
+   if ($dbtype -eq "none")
+   {
+      Copy-Item -LiteralPath 'directory_nodb.xml.template' -Destination 'directory_nodb.xml' -Force
+      continue
+   }
+
+   if (-not $dbdialect.ContainsKey($dbtype))
+   {
+      throw "Unsupported database type '$dbtype' for '$db'."
+   }
+
+   $dbhost = Get-SubVal $db "dbhost" "localhost"
+   $dbport = Get-SubVal $db "dbport" "5432"
+   $dbpath = Get-SubVal $db "dbpath" "../db"
+   $pljava = if ($dbtype -eq "h2") { "TRUE" } else { "FALSE" }
+
+   $jdbc_url = if ($dbtype -eq "h2")
+               {
+                  "h2:$dbpath/$db;DB_CLOSE_DELAY=-1;MV_STORE=FALSE;RTRIM=TRUE"
+               }
+               else
+               {
+                  "postgresql://${dbhost}:${dbport}/$db"
+               }
+
+   $dbfile = "directory_db.xml.$db"
+   Expand-Template 'directory_db.xml.template' @{
+      dbhost        = $dbhost
+      dbport        = $dbport
+      dbname        = $db
+      dbuser        = (Get-SubVal $db "dbuser" "fwd_user")
+      dbuserpass    = (Get-SubVal $db "dbuserpass" "user")
+      dbadmin       = (Get-SubVal $db "dbadmin" "fwd_admin")
+      dbadminpass   = (Get-SubVal $db "dbadminpass" "admin")
+      dbdialect     = $dbdialect[$dbtype]
+      dbdriver      = $dbdriver[$dbtype]
+      max_c3p0_pool = (Get-SubVal $db "max_c3p0_pool" "20")
+      dbpath        = $dbpath
+      jdbc_url      = $jdbc_url
+      pljava        = $pljava
+      collation     = (Get-SubVal $db "collation" "en_US@iso88591_fwd_basic")
+   } $dbfile
+
+   & java -Xmx256m -classpath $p2j_jar com.goldencode.p2j.directory.DirectoryCopy `
+          copy $dbfile /server/standard/database/ directory_tmp.xml /server/standard/database/
+   if ($LASTEXITCODE -ne 0)
+   {
+      throw "DirectoryCopy failed for $db with exit code $LASTEXITCODE."
+   }
+
+   Remove-Item -LiteralPath $dbfile -Force
+}
+
+Move-Item -LiteralPath 'directory_tmp.xml' -Destination $dirfile -Force
+Write-Host "Wrote $dirfile"
+
+exit 0

=== modified file 'deploy/server/server.cmd'
--- old/deploy/server/server.cmd	2025-04-29 05:00:02 +0000
+++ new/deploy/server/server.cmd	2026-08-07 14:04:48 +0000
@@ -1,5 +1,13 @@
 :: Standard FWD server startup script - Wrapper to server.ps1
 @echo off
 setlocal
-pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0server.ps1" %*
-endlocal
+
+:: PowerShell Core when it is installed, otherwise the Windows PowerShell which is preinstalled with
+:: Windows.  %%~$PATH:I expands to the full path of the file when it is found on the PATH, and to an
+:: empty string when it is not.
+set "psexe="
+for %%I in (pwsh.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+if not defined psexe set "psexe=powershell.exe"
+
+"%psexe%" -NoProfile -ExecutionPolicy Bypass -File "%~dp0server.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'deploy/server/server.ps1'
--- old/deploy/server/server.ps1	2026-07-15 05:12:02 +0000
+++ new/deploy/server/server.ps1	2026-08-07 14:04:48 +0000
@@ -126,10 +126,16 @@
       return
    }
 
-   # Resolve the directory.xml path relative to the config's directory.
+   # Resolve the directory.xml path relative to the config's directory.  Split-Path returns an empty
+   # string when the config was found as a bare name in the current directory, which is what happens
+   # when the server is started from within its own directory; Join-Path rejects that.
    $dirXmlPath = $dirXmlName
    if (-not [System.IO.Path]::IsPathRooted($dirXmlPath)) {
-      $dirXmlPath = Join-Path (Split-Path $cfgPath -Parent) $dirXmlName
+      $cfgDir = Split-Path $cfgPath -Parent
+      if ([string]::IsNullOrWhiteSpace($cfgDir)) {
+         $cfgDir = $PWD.Path  # Use current directory if no parent in path
+      }
+      $dirXmlPath = Join-Path $cfgDir $dirXmlName
    }
 
    # Use the same p2j.jar the server resolved (via Get-Classpath) instead of
@@ -321,7 +327,8 @@
       '^--gc$' { $psOptions.EnableGC = $true }
       '^--jmx_port=' { $psOptions.JmxPort = [int]($arg -split '=')[1] }
       '^--clear_logs' {
-         $logDirs = $arg -match '=' ? ($arg -split '=')[1] : $default_log_dirs
+         # an if expression, the ?: ternary operator needs PowerShell 7
+         $logDirs = if ($arg -match '=') { ($arg -split '=')[1] } else { $default_log_dirs }
          Clear-Logs $logDirs
       }
       default {

=== modified file 'deploy/server/start_server.cmd'
--- old/deploy/server/start_server.cmd	2025-04-29 05:00:02 +0000
+++ new/deploy/server/start_server.cmd	2026-08-07 14:04:48 +0000
@@ -4,5 +4,11 @@
 :: Determine script directory
 set scriptdir=%~dp0
 
+:: PowerShell Core when it is installed, otherwise the Windows PowerShell which is preinstalled
+set "psexe="
+for %%I in (pwsh.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+if not defined psexe set "psexe=powershell.exe"
+
 :: Pass all arguments to PowerShell script
-pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%start_server.ps1" %*
+"%psexe%" -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%start_server.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'deploy/server/stop_server.cmd'
--- old/deploy/server/stop_server.cmd	2025-04-29 05:00:02 +0000
+++ new/deploy/server/stop_server.cmd	2026-08-07 14:04:48 +0000
@@ -4,5 +4,11 @@
 :: Determine script directory
 set scriptdir=%~dp0
 
+:: PowerShell Core when it is installed, otherwise the Windows PowerShell which is preinstalled
+set "psexe="
+for %%I in (pwsh.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+if not defined psexe set "psexe=powershell.exe"
+
 :: Pass all arguments to PowerShell script
-pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%stop_server.ps1" %*
+"%psexe%" -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%stop_server.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'hotel.input.windows'
--- old/hotel.input.windows	2017-09-28 16:05:10 +0000
+++ new/hotel.input.windows	2026-08-07 14:04:48 +0000
@@ -1,16 +1,28 @@
 hotel
-
-
+hotel_gui
+*.[fhi]
+*.[pPwW]
 com.goldencode.hotel
-.;common\;
+.:common:
+.;common;
 ADM2.2
 hotel
+hotel
 h2
+localhost
+../db
 fwd_admin
 admin
 fwd_user
 user
-
-
-
-com.goldencode.hotel.Start
+en_US@iso88591_fwd_basic
+20
+no
+mdy
+,
+.
+start.p
+localhost
+7443
+directory.xml
+yes

=== modified file 'install_spawner.cmd'
--- old/install_spawner.cmd	2025-04-29 05:00:02 +0000
+++ new/install_spawner.cmd	2026-08-07 14:04:48 +0000
@@ -4,5 +4,6 @@
 :: Determine script directory
 set scriptdir=%~dp0
 
-:: Pass all arguments to PowerShell script
-pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%scriptdir%install_spawner.ps1" %*
+:: Pass all arguments to PowerShell script, run_powershell.cmd picks the PowerShell which is installed
+call "%scriptdir%run_powershell.cmd" "%scriptdir%install_spawner.ps1" %*
+exit /b %ERRORLEVEL%

=== modified file 'install_spawner.ps1'
--- old/install_spawner.ps1	2025-04-29 05:00:02 +0000
+++ new/install_spawner.ps1	2026-08-07 14:04:48 +0000
@@ -14,14 +14,6 @@
 # Enable strict mode for error handling
 Set-StrictMode -Version Latest
 
-# Ensure the script is running with administrative privileges
-if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
-{
-   Write-Host "This script requires administrative privileges. Restarting with elevation..."
-   Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" -Verb RunAs
-   exit
-}
-
 # Important helper to be included for compatibility with bash sourcing of appname
 function Source-AppName
 {
@@ -45,20 +37,96 @@
 
 function Show-Usage {
    $usage = @(
-      "Usage: $script_name [-a] [-d] [-s<path to place spawner>] [-p<path to FWD>] [-f<path to slf4j-impl.jar>] [-n<path to native spawner>] [-c<path to srv-certs>] [-t<path to postbuild.sh tool>]",
-      "                [-n <native spawner>] [-c <srv-certs.store path>] [-t <postbuild.sh tool path>]",
+      "Usage: $script_name [-a] [-d] [-s <path to place spawner>] [-p <path to FWD>] [-f <path to slf4j-impl.jar>] [-n <path to native spawner>] [-c <path to srv-certs>] [-t <path to postbuild.ps1 tool>]",
       "a = Copy only $appname libraries (prevents co-mingling of application and FWD libraries).",
       "d = Dry run. Just show commands.",
       "s = Destination directory to place spawner.exe (def=$fspawn)",
       "p = Location of FWD libraries p2j.jar and fwd-slf4j.jar (if no -f specified). `"FWD_LIB`" will be used as a fallback if default is not valid. (def=$p2j_lib)",
       "f = Location of file to be placed in slf4j-impl.jar destination (def=fwd-slf4j.jar in -p location or FWD_LIB)",
-      "n = Location of built native spawner (def=$spawn_prog), unless -p specified, which will change the default location)",
+      "n = Location of built native spawner (def=$spawn_prog, unless -p specified, which will change the default location)",
       "c = Location of srv-certs.store file (def=$srv_certs)",
-      "t = Location of postbuild.sh tool (def=$postbuild), unless -p specified, which will change the default location)"
+      "t = Location of postbuild.ps1 tool (def=$postbuild, unless -p specified, which will change the default location)"
    )
    $usage | ForEach-Object { Write-Host $_ }
 }
 
+# Quotes a value for the PowerShell command line assembled for the privileged step below.  A single
+# quote is its own escape, so doubling it is all that is needed.
+function ConvertTo-PSLiteral
+{
+   param (
+      [string]$Value
+   )
+
+   return "'" + ($Value -replace "'", "''") + "'"
+}
+
+<#
+   The counterpart of the "$mysudo" of install_spawner.sh.
+
+   Windows has no sudo, a single command cannot be elevated in place, so the commands which need the
+   rights are grouped into one child process and that process is started elevated.  Everything else
+   in this script stays unelevated, exactly as on Linux: elevating the whole script would also run
+   the jar copies below as an administrator and leave a deployment the developer cannot overwrite on
+   the next build.
+
+   The child is a separate process even when no elevation is needed, because an "exit" of a script
+   called in the current session would end this one as well and skip the library copying below.
+
+   An elevated process cannot share this console: UAC always gives it a window of its own, and
+   -Verb RunAs cannot be combined with -RedirectStandardOutput.  Its output is therefore written to a
+   file and replayed here, otherwise the window closes as soon as the work is done and nothing can be
+   read.  The command is handed over with -EncodedCommand, which takes base64 and so needs no
+   quoting of its own.
+#>
+function Invoke-Privileged
+{
+   param (
+      [string]$Command,
+      [switch]$Elevate
+   )
+
+   # the interpreter running this script, not a hardcoded "pwsh": PowerShell 7 is not necessarily
+   # installed, install_spawner.cmd launches the Windows PowerShell 5.1 instead
+   $psHost = (Get-Process -Id $PID).Path
+
+   if (-not $Elevate)
+   {
+      & $psHost -NoProfile -ExecutionPolicy Bypass -Command "& { $Command }; exit `$LASTEXITCODE"
+      return $LASTEXITCODE
+   }
+
+   Write-Host "Elevation is required for $fspawn, confirm the prompt to continue..."
+
+   $log = Join-Path ([System.IO.Path]::GetTempPath()) "install_spawner_$PID.log"
+
+   # the braces matter, a redirection binds to the last statement only and the command is a list
+   $wrapped = "& { $Command } *> $(ConvertTo-PSLiteral $log); exit `$LASTEXITCODE"
+   $encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wrapped))
+
+   try
+   {
+      $proc = Start-Process -FilePath $psHost `
+                            -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass',
+                                            '-EncodedCommand', $encoded) `
+                            -Verb RunAs -WorkingDirectory $cwd -Wait -PassThru
+   }
+   catch
+   {
+      # declining the UAC prompt lands here, as does a machine with no interactive desktop
+      Write-Host "ERROR: the elevated step could not be started: $($_.Exception.Message)"
+      return 1
+   }
+
+   if (Test-Path -LiteralPath $log)
+   {
+      Get-Content -LiteralPath $log
+      Remove-Item -LiteralPath $log -Force -ErrorAction SilentlyContinue
+   }
+
+   return $proc.ExitCode
+}
+
 # Defaults
 $script_name = $MyInvocation.MyCommand.Name
 $wholePath = $MyInvocation.MyCommand.Path
@@ -74,14 +142,17 @@
    exit 1
 }
 
-# Default paths
-$pwd = Get-Location
-$fspawn = "/opt/spawner"
-$p2j_lib = "$pwd/p2j/build/lib"
-$deploy_home = "$pwd/deploy"
+# Default paths.  Note that $pwd is not used to hold the current directory, it is the name of an
+# automatic PowerShell variable.
+$cwd = (Get-Location).Path
+# The Windows counterparts of the /opt/spawner and /opt/fwd-deploy/spawner of install_spawner.sh.
+$fspawn = Join-Path $env:ProgramData 'FWD\spawner'
+$fwd_deploy = Join-Path $env:ProgramData 'FWD\fwd-deploy\spawner'
+$p2j_lib = "$cwd/p2j/build/lib"
+$deploy_home = "$cwd/deploy"
 $srv_certs = "$deploy_home/server/srv-certs.store"
-$build_home = "$pwd/build"
-$p2j_root = "$pwd/p2j"
+$build_home = "$cwd/build"
+$p2j_root = "$cwd/p2j"
 $postbuild = "$p2j_root/src/native/postbuild.ps1"
 $spawn_prog = "$p2j_root/build/native/spawn.exe"
 $p2j_passed = $false
@@ -114,15 +185,15 @@
 }
 
 # Set defaults when no options passed and spawn/postbuild.ps1 are not found (or bail)
-if (-not $p2j_passed -and -not $spawn_passed -and !(Test-Path $spawn_prog) -and (Test-Path "/opt/fwd-deploy/spawner/spawn")) {
-   $spawn_prog = "/opt/fwd-deploy/spawner/spawn.exe"
+if (-not $p2j_passed -and -not $spawn_passed -and !(Test-Path $spawn_prog) -and (Test-Path "$fwd_deploy/spawn.exe")) {
+   $spawn_prog = "$fwd_deploy/spawn.exe"
 }
 if (!(Test-Path $spawn_prog)) {
    Write-Error "Error: $spawn_prog not found. Exiting."
    exit 1
 }
-if (-not $p2j_passed -and -not $postbuild_passed -and !(Test-Path $postbuild) -and (Test-Path "/opt/fwd-deploy/spawner/postbuild.sh")) {
-   $postbuild = "/opt/fwd-deploy/spawner/postbuild.ps1"
+if (-not $p2j_passed -and -not $postbuild_passed -and !(Test-Path $postbuild) -and (Test-Path "$fwd_deploy/postbuild.ps1")) {
+   $postbuild = "$fwd_deploy/postbuild.ps1"
 }
 if (!(Test-Path $postbuild)) {
    Write-Error "Error: $postbuild not found. Exiting."
@@ -140,7 +211,7 @@
          $spawn_prog = "$p2j_root/build/native/spawn.exe"
       }
       if (-not $postbuild_passed) {
-         $postbuild = "$p2j_root/src/native/postbuild.sh"
+         $postbuild = "$p2j_root/src/native/postbuild.ps1"
       }
    }
 }
@@ -150,21 +221,68 @@
    $slf4j_impl = "$p2j_lib/fwd-slf4j.jar"
 }
 
-# Run postbuild
-$cmdString = "& pwsh -File $postbuild -SpawnDir $fspawn -SpawnProg $spawn_prog -CertFile $srv_certs -FwdLib $p2j_jar"
-if ($d) { 
-   Write-Host $cmdString
+# Elevation is needed only when the destination is outside the user's own profile and this session is
+# not elevated already, the same test as the
+#    [[ "$fspawn"* != "$HOME"* ]] && [[ $(whoami) != "root" ]]
+# of install_spawner.sh.  Installing under -s $env:USERPROFILE\... raises no prompt at all.
+$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
+           ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+$underHome = $fspawn.StartsWith($env:USERPROFILE, [System.StringComparison]::OrdinalIgnoreCase)
+$needElevation = -not $isAdmin -and -not $underHome
+
+# The privileged step: the commands install_spawner.sh runs under sudo, grouped so that at most one
+# UAC prompt is raised.  The order differs from install_spawner.sh in one respect, the directory ACL
+# is applied before postbuild instead of after it: postbuild.ps1 hardens the folder itself and
+# honours its own -StrictAcl, so it has to have the last word.  Against an older postbuild.ps1 which
+# leaves the folder alone, the grant below is still what gives the ordinary users their access.
+$commands = @()
+
+# Create the destination for the spawner, if not already there. postbuild bails out on a missing one,
+# creating it is the job of this script, as in install_spawner.sh.
+if (!(Test-Path $fspawn)) {
+   $commands += "New-Item -Path $(ConvertTo-PSLiteral $fspawn) -ItemType Directory -Force | Out-Null"
+}
+
+# The equivalent of the "chmod gou+rx $fspawn" of install_spawner.sh, S-1-5-32-545 is the well known
+# SID of the Users group (the group names are localized, the SIDs are not).
+$commands += "& icacls $(ConvertTo-PSLiteral $fspawn) /grant '*S-1-5-32-545:(RX)' | Out-Null"
+
+# Position the spawner files
+$commands += ("& $(ConvertTo-PSLiteral $postbuild)" +
+              " -SpawnDir $(ConvertTo-PSLiteral $fspawn)" +
+              " -SpawnProg $(ConvertTo-PSLiteral $spawn_prog)" +
+              " -CertFile $(ConvertTo-PSLiteral $srv_certs)" +
+              " -FwdLib $(ConvertTo-PSLiteral $p2j_jar)" +
+              " -Slf4jImpl $(ConvertTo-PSLiteral $slf4j_impl)")
+
+$privileged = $commands -join '; '
+
+if ($d) {
+   Write-Host $privileged
 } else {
-   & pwsh -File $postbuild -SpawnDir $fspawn -SpawnProg $spawn_prog -CertFile $srv_certs -FwdLib $p2j_jar
+   $rc = Invoke-Privileged -Command $privileged -Elevate:$needElevation
+   if ($rc -ne 0) {
+      Write-Host "WARNING! The spawner installation failed with exit code $rc."
+   }
 }
 
 # Determine the configured JDK
 #jver is 18 for java 1.8, 15 for java 1.5, 110 for java 11, 170 for java 17 etc.
-$jver = & java -version 2>&1 | Select-String 'version' | ForEach-Object {
+# java writes its version banner to stderr, which Windows PowerShell reports as an error record of the
+# native command; keep the preference relaxed for the call and stringify what the merged streams give.
+$previousPreference = $ErrorActionPreference
+$ErrorActionPreference = 'Continue'
+try {
+   $javaVersionLines = & java -version 2>&1 | ForEach-Object { "$_" }
+}
+finally {
+   $ErrorActionPreference = $previousPreference
+}
+$jver = $javaVersionLines | Select-String 'version' | ForEach-Object {
    if ($_ -match '"(\d+\.\d+|\d+)"') {
       $v = $matches[1]
       $parts = $v -split '\.'
-      if ($parts.Length -eq 1) { return "$parts[0]0" }
+      if ($parts.Length -eq 1) { return "$($parts[0])0" }
       return "$($parts[0])$($parts[1])"
    }
 }
@@ -172,24 +290,24 @@
 
 # Copy the application jar and the collation jar specifically to the deployment location
 if (-not (Test-Path $spidir)) {
-   if ($d) { Write-Host "New-Item -Path $spidir -ItemType Directory" } else { New-Item -Path $spidir -ItemType Directory | Out-Null }
+   if ($d) { Write-Host "New-Item -Path $spidir -ItemType Directory -Force" } else { New-Item -Path $spidir -ItemType Directory -Force | Out-Null }
 }
 $appJar = "$build_home/lib/$appname.jar"
 $fwdSpiJar = "$p2j_lib/fwdspi.jar"
 if ($d) {
    Write-Host "Copy-Item $appJar $deploy_home/lib -Force"
-   Write-Host "$fwdSpiJar $spidir -Force"
+   Write-Host "Copy-Item $fwdSpiJar $spidir -Force"
 } else {
    Copy-Item $appJar "$deploy_home/lib" -Force
    Copy-Item $fwdSpiJar $spidir -Force
 }
 # Copy the rest of the library files (except aspectjtools.jar and fwdspi.jar) to the deployment location, unless -a is included.
 if (-not $a) {
-   Get-ChildItem $p2j_lib -Include *.jar,*.zip -Recurse |
+   Get-ChildItem $p2j_lib -Include *.jar,*.zip -Recurse -File |
    Where-Object { $_.Name -notmatch 'aspectjtools\.jar|fwdspi\.jar' } |
    ForEach-Object {
       if ($d) {
-         Write-Host "Copy-Item $_.FullName $deploy_home/lib -Force"
+         Write-Host "Copy-Item $($_.FullName) $deploy_home/lib -Force"
       } else {
          Copy-Item $_.FullName "$deploy_home/lib" -Force
       }

=== added file 'json_template.cmd'
--- old/json_template.cmd	1970-01-01 00:00:00 +0000
+++ new/json_template.cmd	2026-08-07 11:54:43 +0000
@@ -0,0 +1,11 @@
+@echo off
+::
+:: Wrapper for json_template.ps1, the counterpart of json_template.sh.
+::
+:: Run this from the project root, the script resolves its templates relative to the current
+:: directory exactly as the shell version does.
+::
+set scriptdir=%~dp0
+
+call "%scriptdir%run_powershell.cmd" "%scriptdir%json_template.ps1" %*
+exit /b %ERRORLEVEL%

=== added file 'json_template.ps1'
--- old/json_template.ps1	1970-01-01 00:00:00 +0000
+++ new/json_template.ps1	2026-08-07 12:11:51 +0000
@@ -0,0 +1,610 @@
+#
+# The Windows counterpart of json_template.sh, kept in sync with it: takes input from
+# prepare_template.json and performs configuration of build.properties, zfile_set.txt, p2j.cfg.xml,
+# deploy/server/server.xml and the directory.xml file (which can be located anywhere, as it is
+# specified in the json file).
+#
+# Substitution is done with a literal String.Replace and not with the -replace operator: -replace is
+# a regular expression on both sides, so a value holding a $ or a \ would be mangled - a database
+# password is the obvious way to hit that.
+#
+param (
+   [switch]$a,
+   [switch]$d,
+   [switch]$o,
+   [switch]$n,
+   [string]$f
+)
+
+Set-PSDebug -Trace 0
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Show-Usage
+{
+   Write-Host "Usage: json_template.ps1 [-a] [-d] [-o] [-f <json_file>]"
+   Write-Host "Takes input from prepare_template.json and performs configuration of build.properties,"
+   Write-Host "p2j.cfg.xml, deploy/server/server.xml, and the directory.xml file."
+   Write-Host "Where:"
+   Write-Host "   a = Only performing Analytics"
+   Write-Host "   d = Generate deploy/server/prepare_dir.json output file to be used by prepare_dir.ps1"
+   Write-Host "   o = Overwrite the specified directory.xml, if it already exists. Otherwise, leave it alone"
+   Write-Host "   f = Use <json_file> instead of prepare_template.json as input."
+}
+
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+# Property lookup which tolerates a missing key under Set-StrictMode; $cfg.$Key would throw.
+function Get-Node
+{
+   param (
+      $Object,
+      [string]$Key
+   )
+
+   if ($null -eq $Object) { return $null }
+
+   $prop = $Object.PSObject.Properties[$Key]
+   if ($null -eq $prop) { return $null }
+
+   return $prop.Value
+}
+
+function Get-Val
+{
+   param (
+      [string]$Key,
+      [string]$Default = ""
+   )
+
+   $val = Get-Node $script:cfg $Key
+   if ($null -eq $val -or "$val" -eq "") { return $Default }
+
+   return "$val"
+}
+
+# The per-database values live in a sub-object named after the database, so the lookup is two
+# levels.  Note prepare_dir.ps1 used to pass "<db>.<key>" as a single property name, which never
+# resolved and silently handed back every default.
+function Get-SubVal
+{
+   param (
+      [string]$SubName,
+      [string]$Key,
+      [string]$Default = ""
+   )
+
+   $node = Get-Node $script:cfg $SubName
+   $val = Get-Node $node $Key
+   if ($null -eq $val -or "$val" -eq "") { return $Default }
+
+   return "$val"
+}
+
+# Replaces every {key} of a template with its value and writes the result.
+function Expand-Template
+{
+   param (
+      [string]$Template,
+      [hashtable]$Values,
+      [string]$Destination
+   )
+
+   if (!(Test-Path -LiteralPath $Template -PathType Leaf))
+   {
+      throw "Could not create $Destination, the template $Template was not found."
+   }
+
+   $content = [System.IO.File]::ReadAllText($Template)
+
+   foreach ($key in $Values.Keys)
+   {
+      $content = $content.Replace("{$key}", [string]$Values[$key])
+   }
+
+   [System.IO.File]::WriteAllText($Destination, $content, $utf8NoBom)
+}
+
+# Splits a PROPATH answer.  Both spellings are accepted so the same answer file works either way:
+# a ';' separated list is split on ';' (which also keeps a "C:\..." drive letter intact), anything
+# else is split on ':'.
+function Split-PropathValue
+{
+   param (
+      [string]$Value
+   )
+
+   $sep = if ($Value.Contains(';')) { ';' } else { ':' }
+
+   return @($Value -split [regex]::Escape($sep) | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" })
+}
+
+if ($args -contains "-?" -or $args -contains "/?" -or $args -contains "-h")
+{
+   Show-Usage
+   exit 1
+}
+
+$analytics = $a.IsPresent
+$overwrite = $o.IsPresent
+$json_file = if ($f) { $f } else { "prepare_template.json" }
+$prepare_dir = if ($d) { "deploy/server/prepare_dir.json" } else { "" }
+
+if (!(Test-Path -LiteralPath $json_file -PathType Leaf))
+{
+   Write-Error "Input file not found: $json_file"
+   Show-Usage
+   exit 1
+}
+
+$root = (Get-Location).Path
+
+# System.IO and every native command resolve a relative path against the PROCESS working directory,
+# which PowerShell's own location does not track: Push-Location moves the latter and leaves the
+# former where the script started.  The two are kept in step here and around the Push-Location
+# further down, otherwise the templates under deploy/server are looked for in the project root and
+# the files written there land in the root as well.
+[Environment]::CurrentDirectory = $root
+
+$cfg = [System.IO.File]::ReadAllText($json_file) | ConvertFrom-Json
+
+# ---------------------------------------------------------------------------------------------------
+# Values taken straight from the json
+# ---------------------------------------------------------------------------------------------------
+$appname = Get-Val "appname"
+$projname = Get-Val "projname" $appname
+$include_spec = Get-Val "include_spec" "*.[fhi]"
+$program_spec = Get-Val "program_spec" "*.[pPwW]"
+$pkgroot = if ($analytics) { "com.goldencode.$appname" } else { Get-Val "pkgroot" "com.goldencode.$appname" }
+$propath_bld = Get-Val "propath_bld" "."
+$propath = Get-Val "propath" "."
+$admversion = Get-Val "admversion" "ADM2.2"
+$dbnames = Get-Val "dbnames"
+$embedded_host = Get-Val "embedded_host" "localhost"
+$admin_port = Get-Val "admin_port" "7443"
+
+if ($analytics)
+{
+   $win_proj = "no"
+   $dateFormat = "mdy"
+   $numberGroupSep = ","
+   $numberDecimalSep = "."
+   $p2j_entry = ""
+   $directory_xml_file = "directory.xml"
+}
+else
+{
+   $win_proj = Get-Val "win_proj" "no"
+   $dateFormat = Get-Val "dateFormat" "mdy"
+   $numberGroupSep = Get-Val "numberGroupSep" ","
+   $numberDecimalSep = Get-Val "numberDecimalSep" "."
+   $p2j_entry = Get-Val "p2j_entry"
+   $directory_xml_file = Get-Val "directory_xml_file" "directory.xml"
+}
+
+if ([string]::IsNullOrEmpty($appname))
+{
+   Write-Error "No 'appname' in $json_file."
+   exit 1
+}
+if ([string]::IsNullOrEmpty($dbnames))
+{
+   Write-Error "No 'dbnames' in $json_file."
+   exit 1
+}
+
+# dbnames is a comma separated string in prepare_template.json, but the prepare_dir_*.json files
+# carry a real JSON array; accept either.
+$dbnamesNode = Get-Node $cfg "dbnames"
+if ($dbnamesNode -is [System.Array])
+{
+   $db_array = @($dbnamesNode | ForEach-Object { "$_".Trim() } | Where-Object { $_ -ne "" })
+   $dbnames = $db_array -join ','
+}
+else
+{
+   $db_array = @($dbnames -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" })
+}
+$defdatabase = $db_array[0]
+
+# ---------------------------------------------------------------------------------------------------
+# Conversion side values: these describe the system the ABL application came FROM, so they follow
+# win_proj.  Note that cfg/p2j.cfg.xml.template currently hardcodes opsys/winsys, the two below
+# reach it only if that template is given {opsys}/{winsys} markers.
+# ---------------------------------------------------------------------------------------------------
+if ($win_proj -eq "yes")
+{
+   $opsys = "WIN32"
+   $winsys = "MS-WINDOWS"
+   $unix_escapes = "FALSE"
+}
+else
+{
+   $opsys = "UNIX"
+   $winsys = ""
+   $unix_escapes = "TRUE"
+}
+
+# ---------------------------------------------------------------------------------------------------
+# Runtime side values: these describe the machine the FWD server will run ON, so they follow this
+# host and NOT win_proj, and each one can be pinned from the json.
+#
+# json_template.sh drives all of them off win_proj, which is the question "did the ABL application
+# run on Windows" - a property of the source system.  The separators, the case sensitivity, the
+# spawner and the client working directory belong to the runtime host instead, which is why a
+# directory.xml generated on Linux is wrong in every one of them once it is moved to Windows.
+# ---------------------------------------------------------------------------------------------------
+$path_separator = Get-Val "path_separator" ";"
+$file_separator = Get-Val "file_separator" "\"
+$case_sensitive = Get-Val "case_sensitive" "FALSE"
+$spawner_path = Get-Val "spawner_path" (Join-Path $env:ProgramData 'FWD\spawner\spawn.exe')
+$client_start_dir = Get-Val "client_start_dir" (Join-Path $root 'deploy\client')
+$server_log = Get-Val "server_log" "../logs"
+$client_log = Get-Val "client_log" "../logs"
+$keyboard = Get-Val "kbd_layout" "US"
+$user = Get-Val "os_user" $env:USERNAME
+
+# ---------------------------------------------------------------------------------------------------
+# Derived values
+# ---------------------------------------------------------------------------------------------------
+$pkgrootfolder = $pkgroot.Replace('.', '/')
+
+# The p2j.cfg.xml propath keeps ':' and '/' on every platform: the patpath parameter sitting right
+# above it in the template is hardcoded that way and is not templated, and the retired
+# prepare_template.cmd emitted ':' here too.  Only the runtime search path below follows the host.
+$propathcfg = '${P2J_HOME}:${P2J_HOME}/abl'
+$searchpath = "."
+foreach ($pentry in (Split-PropathValue $propath_bld))
+{
+   if ($pentry -ne ".")
+   {
+      $cfgentry = $pentry.Replace('\', '/')
+      $propathcfg = "${propathcfg}:`${P2J_HOME}/abl/$cfgentry"
+
+      # json_template.sh joins the search path with a hardcoded ':' even for a Windows project;
+      # it belongs to the runtime, so it follows the runtime separator here.
+      $searchpath = "$searchpath$path_separator$cfgentry"
+   }
+}
+
+# AppCDS, only when the json carries the block
+$appcdsNode = Get-Node $cfg "appcds"
+if ($null -ne $appcdsNode)
+{
+   $client_lib_path = Get-SubVal "appcds" "client_lib_path" (Join-Path $root 'deploy\lib')
+   $webclient_appcds_archive = Get-SubVal "appcds" "webclient_appcds_archive" `
+                                          (Join-Path $root 'appcds\client\default.jsa')
+   $webclient_appcds_template = Get-SubVal "appcds" "webclient_appcds_jvmargs" `
+                                           "-Xshare:auto -XX:SharedArchiveFile={archive}"
+   $webclient_appcds_jvmargs = $webclient_appcds_template.Replace("{archive}", $webclient_appcds_archive)
+   $libpath_rendered = @"
+          <node class="string" name="libPath">
+            <node-attribute name="value" value="$client_lib_path"/>
+          </node>
+"@
+}
+else
+{
+   $client_lib_path = Join-Path $root 'deploy\lib'
+   $webclient_appcds_archive = ""
+   $webclient_appcds_jvmargs = ""
+   $libpath_rendered = ""
+}
+$webclient_memory = Get-Val "webclient_memory" "128m"
+
+# First DB is the default, so it populates the values used in non-DB specific configs
+$dbtype = Get-SubVal $defdatabase "dbtype" "h2"
+$dbuser = Get-SubVal $defdatabase "dbuser" "fwd_user"
+$dbuserpass = Get-SubVal $defdatabase "dbuserpass" "user"
+$dbadmin = Get-SubVal $defdatabase "dbadmin" "fwd_admin"
+$dbadminpass = Get-SubVal $defdatabase "dbadminpass" "admin"
+$dbhost = Get-SubVal $defdatabase "dbhost" "localhost"
+$dbport = Get-SubVal $defdatabase "dbport" "5433"
+
+$dbh2 = if ($dbtype -eq "h2") { "true" } else { "false" }
+$dbpostgres = if ($dbtype -eq "postgres") { "true" } else { "false" }
+$dbmariadb = if ($dbtype -eq "mariadb") { "true" } else { "false" }
+
+# ---------------------------------------------------------------------------------------------------
+# build.properties
+# ---------------------------------------------------------------------------------------------------
+Expand-Template 'build.properties.template' @{
+   appname        = $appname
+   projname       = $projname
+   program_spec   = $program_spec
+   dbh2           = $dbh2
+   dbpostgres     = $dbpostgres
+   dbmariadb      = $dbmariadb
+   dbhost         = $dbhost
+   dbport         = $dbport
+   dbnames        = $dbnames
+   dbuser         = $dbuser
+   dbuserpass     = $dbuserpass
+   dbadmin        = $dbadmin
+   dbadminpass    = $dbadminpass
+   pkgrootfolder  = $pkgrootfolder
+   adm_version    = $admversion
+} 'build.properties'
+
+# ---------------------------------------------------------------------------------------------------
+# zfile_set.txt, only when the template is present
+# ---------------------------------------------------------------------------------------------------
+if (Test-Path -LiteralPath 'zfile_set.txt.template' -PathType Leaf)
+{
+   Expand-Template 'zfile_set.txt.template' @{ program_spec = $program_spec } 'zfile_set.txt'
+}
+
+# ---------------------------------------------------------------------------------------------------
+# cfg/p2j.cfg.xml, with the per-database namespaces folded in
+# ---------------------------------------------------------------------------------------------------
+$namespaces = New-Object System.Collections.Generic.List[string]
+foreach ($item in $db_array)
+{
+   $nsTemplate = [System.IO.File]::ReadAllText('cfg/p2j.namespace.xml.template')
+   $nsTemplate = $nsTemplate.Replace('{dbname}', $item)
+   $nsTemplate = $nsTemplate.Replace('{dbimport_file}', (Get-SubVal $item "dbimport_file" $item))
+   $nsTemplate = $nsTemplate.Replace('{collation}', (Get-SubVal $item "collation" "en_US@iso88591_fwd_basic"))
+   $namespaces.Add($nsTemplate.TrimEnd())
+}
+
+Expand-Template 'cfg/p2j.cfg.xml.template' @{
+   propath        = $propathcfg
+   include_spec   = $include_spec
+   pkgroot        = $pkgroot
+   program_spec   = $program_spec
+   path_separator = $path_separator
+   file_separator = $file_separator
+   case_sensitive = $case_sensitive
+   unix_escapes   = $unix_escapes
+   opsys          = $opsys
+   winsys         = $winsys
+   dbnamespaces   = ($namespaces -join [Environment]::NewLine)
+} 'cfg/p2j.cfg.xml'
+
+# ---------------------------------------------------------------------------------------------------
+# The manifest
+# ---------------------------------------------------------------------------------------------------
+if (!(Test-Path -LiteralPath 'manifest'))
+{
+   New-Item -Path 'manifest' -ItemType Directory -Force | Out-Null
+}
+[System.IO.File]::WriteAllText((Join-Path $root "manifest\$appname.mf"),
+                               "Class-Path: $appname.jar" + [Environment]::NewLine,
+                               $utf8NoBom)
+
+# ---------------------------------------------------------------------------------------------------
+# server.xml and directory.xml, both relative to deploy/server
+# ---------------------------------------------------------------------------------------------------
+$serverDir = Join-Path $root 'deploy\server'
+Push-Location -LiteralPath $serverDir
+[Environment]::CurrentDirectory = $serverDir
+try
+{
+   Expand-Template 'server.xml.template' @{ directory_xml_file = $directory_xml_file } 'server.xml'
+
+   # Where the directory.xml ends up; a bare file name has no parent, which Join-Path rejects.
+   $dirParent = Split-Path -Path $directory_xml_file -Parent
+   if ([string]::IsNullOrWhiteSpace($dirParent)) { $dirParent = "." }
+   if (!(Test-Path -LiteralPath $dirParent -PathType Container))
+   {
+      throw "The directory holding $directory_xml_file does not exist: $dirParent"
+   }
+   $dirfile = Join-Path (Resolve-Path -LiteralPath $dirParent).Path `
+                        (Split-Path -Path $directory_xml_file -Leaf)
+
+   if ((Test-Path -LiteralPath $dirfile) -and -not $overwrite)
+   {
+      Write-Error "ERROR: Output file $dirfile exists, and '-o' not specified."
+      Show-Usage
+      exit 1
+   }
+
+   Expand-Template 'directory.xml.template' @{
+      spawner_path             = $spawner_path
+      client_start_dir         = $client_start_dir
+      libPath                  = $libpath_rendered
+      dateFormat               = $dateFormat
+      numberGroupSep           = $numberGroupSep
+      numberDecimalSep         = $numberDecimalSep
+      p2j_entry                = $p2j_entry
+      pkgroot                  = $pkgroot
+      propath                  = $propath
+      search_path              = $searchpath
+      path_separator           = $path_separator
+      file_separator           = $file_separator
+      case_sensitive           = $case_sensitive
+      os_user                  = $user
+      kbd_layout               = $keyboard
+      dbname                   = $defdatabase
+      server_log               = $server_log
+      client_log               = $client_log
+      embedded_host            = $embedded_host
+      admin_port               = $admin_port
+      webclient_appcds_jvmargs = $webclient_appcds_jvmargs
+      webclient_memory         = $webclient_memory
+   } 'directory_tmp.xml'
+
+   # ------------------------------------------------------------------------------------------------
+   # Merge each database into the directory
+   # ------------------------------------------------------------------------------------------------
+   $dbdialect = @{
+      h2       = "com.goldencode.p2j.persist.dialect.P2JH2Dialect"
+      postgres = "com.goldencode.p2j.persist.dialect.P2JPostgreSQLDialect"
+   }
+   $dbdriver = @{
+      h2       = "org.h2.Driver"
+      postgres = "org.postgresql.Driver"
+   }
+
+   $fwd_lib = if ($env:FWD_LIB) { $env:FWD_LIB } else { "../../p2j" }
+   if (!(Test-Path -LiteralPath $fwd_lib))
+   {
+      throw "Either FWD_LIB must be set to a valid directory or ./p2j must exist so as to locate p2j.jar"
+   }
+   $p2j_jar = if (Test-Path -LiteralPath "$fwd_lib/build/lib/p2j.jar") { "$fwd_lib/build/lib/p2j.jar" }
+              elseif (Test-Path -LiteralPath "$fwd_lib/lib/p2j.jar")   { "$fwd_lib/lib/p2j.jar" }
+              else                                                     { "../../p2j/lib/p2j.jar" }
+
+   foreach ($item in $db_array)
+   {
+      $itemType = Get-SubVal $item "dbtype" "h2"
+      $itemPath = Get-SubVal $item "dbpath" "../db"
+      $itemHost = Get-SubVal $item "dbhost" "localhost"
+      $itemPort = Get-SubVal $item "dbport" "5433"
+      $pljava = if ($itemType -eq "h2") { "TRUE" } else { "FALSE" }
+
+      $jdbc_url = if ($itemType -eq "h2")
+                  {
+                     "h2:$itemPath/$item;DB_CLOSE_DELAY=-1;MV_STORE=FALSE;RTRIM=TRUE"
+                  }
+                  else
+                  {
+                     "postgresql://${itemHost}:${itemPort}/$item"
+                  }
+
+      if (-not $dbdialect.ContainsKey($itemType))
+      {
+         throw "Unsupported database type '$itemType' for '$item'."
+      }
+
+      $dbfile = "directory_db.xml.$item"
+      Expand-Template 'directory_db.xml.template' @{
+         dbhost        = $itemHost
+         dbport        = $itemPort
+         dbname        = $item
+         dbuser        = (Get-SubVal $item "dbuser" "fwd_user")
+         dbuserpass    = (Get-SubVal $item "dbuserpass" "user")
+         dbadmin       = (Get-SubVal $item "dbadmin" "fwd_admin")
+         dbadminpass   = (Get-SubVal $item "dbadminpass" "admin")
+         dbdialect     = $dbdialect[$itemType]
+         dbdriver      = $dbdriver[$itemType]
+         max_c3p0_pool = (Get-SubVal $item "max_c3p0_pool" "20")
+         dbpath        = $itemPath
+         jdbc_url      = $jdbc_url
+         pljava        = $pljava
+         collation     = (Get-SubVal $item "collation" "en_US@iso88591_fwd_basic")
+      } $dbfile
+
+      & java -Xmx256m -classpath $p2j_jar com.goldencode.p2j.directory.DirectoryCopy `
+             copy $dbfile /server/standard/database/ directory_tmp.xml /server/standard/database/
+      if ($LASTEXITCODE -ne 0)
+      {
+         throw "DirectoryCopy failed for $item with exit code $LASTEXITCODE."
+      }
+
+      Remove-Item -LiteralPath $dbfile -Force
+   }
+
+   Move-Item -LiteralPath 'directory_tmp.xml' -Destination $dirfile -Force
+   Write-Host "Wrote $dirfile"
+}
+finally
+{
+   Pop-Location
+   [Environment]::CurrentDirectory = (Get-Location).Path
+}
+
+# ---------------------------------------------------------------------------------------------------
+# prepare_dir.json, if requested
+# ---------------------------------------------------------------------------------------------------
+if ($prepare_dir)
+{
+   $out = [ordered]@{
+      directory_xml_file = $directory_xml_file
+      spawner_path       = $spawner_path
+      client_start_dir   = $client_start_dir
+      appcds             = [ordered]@{
+         client_lib_path          = $client_lib_path
+         webclient_appcds_jvmargs = $webclient_appcds_jvmargs
+         webclient_appcds_archive = $webclient_appcds_archive
+      }
+      webclient_memory   = $webclient_memory
+      dateFormat         = $dateFormat
+      numberGroupSep     = $numberGroupSep
+      numberDecimalSep   = $numberDecimalSep
+      p2j_entry          = $p2j_entry
+      pkgroot            = $pkgroot
+      propath            = $propath
+      search_path        = $searchpath
+      path_separator     = $path_separator
+      file_separator     = $file_separator
+      case_sensitive     = $case_sensitive
+      os_user            = $user
+      kbd_layout         = $keyboard
+      server_log         = $server_log
+      client_log         = $client_log
+      embedded_host      = $embedded_host
+      admin_port         = $admin_port
+      dbnames            = $dbnames
+   }
+
+   foreach ($item in $db_array)
+   {
+      $itemType = Get-SubVal $item "dbtype" "h2"
+      $sub = [ordered]@{
+         dbtype        = $itemType
+         dbuser        = (Get-SubVal $item "dbuser" "fwd_user")
+         dbuserpass    = (Get-SubVal $item "dbuserpass" "user")
+         dbadmin       = (Get-SubVal $item "dbadmin" "fwd_admin")
+         dbadminpass   = (Get-SubVal $item "dbadminpass" "admin")
+         max_c3p0_pool = (Get-SubVal $item "max_c3p0_pool" "20")
+         collation     = (Get-SubVal $item "collation" "en_US@iso88591_fwd_basic")
+         dbhost        = (Get-SubVal $item "dbhost" "localhost")
+      }
+      if ($itemType -eq "postgres") { $sub["dbport"] = (Get-SubVal $item "dbport" "5433") }
+      elseif ($itemType -eq "h2")   { $sub["dbpath"] = (Get-SubVal $item "dbpath" "../db") }
+
+      $out[$item] = $sub
+   }
+
+   [System.IO.File]::WriteAllText((Join-Path $root $prepare_dir),
+                                  ($out | ConvertTo-Json -Depth 10),
+                                  $utf8NoBom)
+   Write-Host "Wrote $prepare_dir"
+}
+
+# ---------------------------------------------------------------------------------------------------
+# If this isn't the hotel application, the user is creating a new project, so cleanup hotel artifacts
+# ---------------------------------------------------------------------------------------------------
+if ($appname -ne "hotel")
+{
+   $drop = @('hotel.bat', 'hotel.ini', 'hotel.input.linux', 'hotel.input.windows', 'hotel.pf',
+             'prepare_hotel.cmd', 'prepare_hotel.sh', 'data/hotel.df', 'manifest/hotel.mf',
+             'prepare_template.cmd', 'prepare_template.ps1', 'prepare_template.sh',
+             'prepare_json.cmd', 'prepare_json.ps1', 'prepare_json.sh',
+             'json_template.cmd', 'json_template.ps1', 'json_template.sh',
+             'start_hotel_abl.cmd', 'src/text-metrics.xml')
+   foreach ($item in $drop)
+   {
+      if (Test-Path -LiteralPath $item) { Remove-Item -LiteralPath $item -Force -Recurse }
+   }
+   Get-ChildItem -Path . -Filter 'components-4gl-fwd.*' -File -ErrorAction SilentlyContinue |
+      Remove-Item -Force
+   Get-ChildItem -Path . -Filter 'components-fwd-4gl.*' -File -ErrorAction SilentlyContinue |
+      Remove-Item -Force
+
+   foreach ($dir in @('data/dump/hotel', 'abldb'))
+   {
+      if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force }
+   }
+   if (Test-Path -LiteralPath 'abl')
+   {
+      Get-ChildItem -Path 'abl' -Force | Remove-Item -Recurse -Force
+   }
+
+   Get-ChildItem -Path 'cfg', 'deploy/server', '.' -Filter '*.template' -File -ErrorAction SilentlyContinue |
+      Remove-Item -Force
+
+   [System.IO.File]::WriteAllText((Join-Path $root 'appname'),
+                                  "appname=$appname" + [Environment]::NewLine, $utf8NoBom)
+   Copy-Item -LiteralPath 'appname' -Destination 'deploy/client/appname' -Force
+   Copy-Item -LiteralPath 'appname' -Destination 'deploy/server/appname' -Force
+
+   $hotelAdmin = 'docker/repo/scripts/hotel_admin'
+   if (Test-Path -LiteralPath $hotelAdmin)
+   {
+      Move-Item -LiteralPath $hotelAdmin -Destination "docker/repo/scripts/${appname}_admin" -Force
+   }
+}
+
+exit 0

=== modified file 'prepare_hotel.cmd'
--- old/prepare_hotel.cmd	2017-03-15 13:35:33 +0000
+++ new/prepare_hotel.cmd	2026-08-07 14:04:48 +0000
@@ -1,2 +1,12 @@
 @echo off
-call prepare_template.cmd -n < hotel.input.windows 1> nul
\ No newline at end of file
+::
+:: The counterpart of prepare_hotel.sh: feeds the recorded answers to prepare_template.cmd.
+::
+:: The output is NOT discarded, unlike the "1> nul" this used to carry: prepare_json.ps1 writes its
+:: prompts to stderr and its data to the json, so stdout holds only the progress and the errors,
+:: which are the reason to run this in the first place.
+::
+set scriptdir=%~dp0
+
+call "%scriptdir%prepare_template.cmd" -n < "%scriptdir%hotel.input.windows"
+exit /b %ERRORLEVEL%

=== added file 'prepare_json.cmd'
--- old/prepare_json.cmd	1970-01-01 00:00:00 +0000
+++ new/prepare_json.cmd	2026-08-07 14:04:48 +0000
@@ -0,0 +1,11 @@
+@echo off
+::
+:: Wrapper for prepare_json.ps1, the counterpart of prepare_json.sh.
+::
+:: Run this from the project root, the script resolves its templates relative to the current
+:: directory exactly as the shell version does.
+::
+set scriptdir=%~dp0
+
+call "%scriptdir%run_powershell.cmd" "%scriptdir%prepare_json.ps1" %*
+exit /b %ERRORLEVEL%

=== added file 'prepare_json.ps1'
--- old/prepare_json.ps1	1970-01-01 00:00:00 +0000
+++ new/prepare_json.ps1	2026-08-07 14:04:48 +0000
@@ -0,0 +1,266 @@
+#
+# The Windows counterpart of prepare_json.sh, kept in sync with it: takes input, either from the user
+# or redirected from a file, and creates prepare_template.json to hold the values that were entered.
+# That file is the input of json_template.ps1.
+#
+# The prompt order here MUST match prepare_json.sh exactly, the *.input.* files are positional.
+#
+param (
+   [switch]$n,
+   [switch]$a
+)
+
+Set-PSDebug -Trace 0
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$JsonFile = "prepare_template.json"
+
+function Show-Usage
+{
+   Write-Host "Usage: prepare_json.ps1 [-n] [-a]"
+   Write-Host "Takes input, either from the user or redirected from a file, and creates $JsonFile"
+   Write-Host "to hold the values that were entered. This file is used as input to json_template.ps1"
+   Write-Host "Where:"
+   Write-Host "   n = No prompting. Typically used when taking input from a redirected file."
+   Write-Host "   a = Only performing Analytics"
+}
+
+# Reads one answer.
+#
+# The prompt goes to stderr, as the "read -p" of bash does, so a caller which captures stdout gets
+# only the data.  [Console]::In.ReadLine() is used rather than Read-Host because it reads a
+# redirected stdin as happily as a console one, which is how the *.input.* files are fed in.
+#
+# An empty reply takes the default, which is what the "read -e -i <default>" of prepare_json.sh does
+# interactively: readline pre-fills the line with the default, so a bare Enter submits it.  Note the
+# one divergence, a deliberately empty line in a redirected input file also takes the default here,
+# where bash would store the empty string.
+function Read-Value
+{
+   param (
+      [string]$Prompt,
+      [string]$Default = ""
+   )
+
+   if ([string]::IsNullOrEmpty($Default))
+   {
+      [Console]::Error.Write($Prompt)
+   }
+   else
+   {
+      [Console]::Error.Write("$Prompt[$Default] ")
+   }
+
+   $line = [Console]::In.ReadLine()
+
+   if ($null -eq $line)
+   {
+      [Console]::Error.WriteLine()
+      throw ("End of input reached, $JsonFile is incomplete. The input file has fewer answers " +
+             "than this script has questions.")
+   }
+
+   $line = $line.Trim()
+   $value = if ($line -eq "") { $Default } else { $line }
+
+   # With the answers coming from a redirected file nothing is echoed back, which runs every prompt
+   # and its answer together into one unreadable paragraph.  Echo what was actually taken; an
+   # interactive console already echoes the typing, so it is left alone there.
+   if ([Console]::IsInputRedirected)
+   {
+      [Console]::Error.WriteLine($value)
+   }
+
+   return $value
+}
+
+# The JSON is assembled in an ordered map and written once at the end, rather than rewritten after
+# every answer the way the jq of prepare_json.sh does.
+$json = [ordered]@{}
+
+function Set-Val
+{
+   param (
+      [string]$Key,
+      [string]$Value
+   )
+
+   $script:json[$Key] = $Value
+}
+
+function Set-SubVal
+{
+   param (
+      [string]$SubName,
+      [string]$Key,
+      [string]$Value
+   )
+
+   if (-not $script:json.Contains($SubName))
+   {
+      $script:json[$SubName] = [ordered]@{}
+   }
+
+   $script:json[$SubName][$Key] = $Value
+}
+
+if ($args -contains "-?" -or $args -contains "/?" -or $args -contains "-h")
+{
+   Show-Usage
+   exit 1
+}
+
+$verbose = -not $n
+$analytics = $a.IsPresent
+
+# Defaults, the same values prepare_json.sh declares.
+$include_spec = "*.[fhi]"
+$program_spec = "(*.[pPwW]|*.cls|*.htm|*html)"
+$propath_bld = ".:"
+$propath = ".:"
+$dbtype = "h2"
+$dbhost = "localhost"
+$dbport = "5432"
+$dbadmin = "fwd_admin"
+$dbadminpass = "admin"
+$dbuser = "fwd_user"
+$dbuserpass = "user"
+$collation = "en_US@iso88591_fwd_basic"
+$max_c3p0_pool = "20"
+$win_proj = "no"
+$dateFormat = "mdy"
+$numberGroupSep = ","
+$numberDecimalSep = "."
+$p2j_entry = ""
+$embedded_host = "localhost"
+$admin_port = "7443"
+$directory_xml_file = "directory.xml"
+$webclient_memory = "128m"
+
+$cwd = (Get-Location).Path
+$client_lib_path = Join-Path $cwd 'deploy\lib'
+$webclient_appcds_jvmargs = "-Xshare:auto -XX:SharedArchiveFile={archive}"
+$webclient_appcds_archive = Join-Path $cwd 'appcds\client\default.jsa'
+
+# Start with fresh json
+if (Test-Path -LiteralPath $JsonFile)
+{
+   Remove-Item -LiteralPath $JsonFile -Force
+}
+
+$appname = Read-Value "Enter your application name (only letters or digits, first character a letter): "
+Set-Val "appname" $appname
+$projname = Read-Value ("Enter your project name (can be different from the application) " +
+                        "(only letters or digits, first character a letter): ") $appname
+Set-Val "projname" $projname
+$pkgroot = "com.goldencode.$appname"
+
+# 1. conversion inputs
+if ($verbose) { Write-Host "1. Enter conversion configurations for your project" }
+$include_spec = Read-Value ("Edit the regex which matches all your include files " +
+                            "(enter for default): ") $include_spec
+Set-Val "include_spec" $include_spec
+$program_spec = Read-Value ("Edit the regex which matches all your program files " +
+                            "(enter for default): ") $program_spec
+Set-Val "program_spec" $program_spec
+if (-not $analytics)
+{
+   $pkgroot = Read-Value "Edit the root package name - Java format (enter for default): " $pkgroot
+   Set-Val "pkgroot" $pkgroot
+}
+# pkgrootfolder - automated
+$propath_bld = Read-Value ("Enter the PROPATH from your progress.ini file used for the application " +
+                           "build (enter for default): ") $propath_bld
+Set-Val "propath_bld" $propath_bld
+$propath = Read-Value ("Enter the PROPATH from your progress.ini file used for the application " +
+                       "runtime (enter for default): ") $propath
+Set-Val "propath" $propath
+$admversion = Read-Value "Enter the ADM version: " "ADM2.2"
+Set-Val "admversion" $admversion
+
+# 2. database inputs
+if ($verbose) { Write-Host "2. Enter database configuration for your project" }
+$dbnames = Read-Value "Enter the legacy database name(s), separated by a comma (default DB first): " ""
+Set-Val "dbnames" $dbnames
+$db_array = @($dbnames -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" })
+
+if (-not $analytics)
+{
+   foreach ($item in $db_array)
+   {
+      $dbimport_file = Read-Value "Enter the database import file for `"$item`": " $item
+      Set-SubVal $item "dbimport_file" $dbimport_file
+      $dbtype = Read-Value "Enter the database type of `"$item`" (h2 or postgres): " $dbtype
+      Set-SubVal $item "dbtype" $dbtype
+      $dbhost = Read-Value "Edit the DB host name for your imported database `"$item`": " $dbhost
+      Set-SubVal $item "dbhost" $dbhost
+      if ($dbtype -eq "postgres")
+      {
+         $dbport = Read-Value "Edit the PostgreSQL port for your imported database `"$item`": " $dbport
+         Set-SubVal $item "dbport" $dbport
+      }
+      elseif ($dbtype -eq "h2")
+      {
+         $dbpath = Read-Value "Edit the H2 database path for your imported database `"$item`": " "../db/"
+         Set-SubVal $item "dbpath" $dbpath
+      }
+      $dbadmin = Read-Value "Enter the SQL admin for your imported database `"$item`": " $dbadmin
+      Set-SubVal $item "dbadmin" $dbadmin
+      $dbadminpass = Read-Value ("Enter the SQL admin password for your imported database " +
+                                 "`"$item`": ") $dbadminpass
+      Set-SubVal $item "dbadminpass" $dbadminpass
+      $dbuser = Read-Value "Enter the SQL user for your imported database `"$item`": " $dbuser
+      Set-SubVal $item "dbuser" $dbuser
+      $dbuserpass = Read-Value ("Enter the SQL user password for your imported database " +
+                                "`"$item`": ") $dbuserpass
+      Set-SubVal $item "dbuserpass" $dbuserpass
+      $collation = Read-Value "Enter the collation scheme for your imported database `"$item`": " $collation
+      Set-SubVal $item "collation" $collation
+      $max_c3p0_pool = Read-Value ("Enter the C3P0 Max Pool Size for your imported database " +
+                                   "`"$item`": ") $max_c3p0_pool
+      Set-SubVal $item "max_c3p0_pool" $max_c3p0_pool
+   }
+
+   # 3. runtime setup
+   if ($verbose) { Write-Host "3. Enter runtime configuration for your project" }
+   $win_proj = Read-Value ("Does your ABL application currently run on the Windows OS " +
+                           "(answer yes/no): ") $win_proj
+   Set-Val "win_proj" $win_proj
+   $dateFormat = Read-Value "Enter the date format from your progress.ini (enter for default): " $dateFormat
+   Set-Val "dateFormat" $dateFormat
+   $numberGroupSep = Read-Value ("Enter the number group separator from your progress.ini " +
+                                 "(enter for default): ") $numberGroupSep
+   Set-Val "numberGroupSep" $numberGroupSep
+   $numberDecimalSep = Read-Value ("Enter the number decimal separator from your progress.ini " +
+                                   "(enter for default): ") $numberDecimalSep
+   Set-Val "numberDecimalSep" $numberDecimalSep
+   $p2j_entry = Read-Value ("Enter the procedure name containing the entry point for your " +
+                            "application: ") $p2j_entry
+   Set-Val "p2j_entry" $p2j_entry
+   $embedded_host = Read-Value "Enter the hostname for the embedded web server: " $embedded_host
+   Set-Val "embedded_host" $embedded_host
+   $admin_port = Read-Value "Enter the port to use for web client access: " $admin_port
+   Set-Val "admin_port" $admin_port
+   $directory_xml_file = Read-Value ("Enter the directory filename, including path (relative to the " +
+                                     "server.xml file location.) (enter for default): ") $directory_xml_file
+   Set-Val "directory_xml_file" $directory_xml_file
+   $answer = Read-Value "Do you want a sample AppCDS setup (answer yes/no): " "no"
+   if ($answer.ToLower() -eq "yes")
+   {
+      Set-SubVal "appcds" "client_lib_path" $client_lib_path
+      Set-SubVal "appcds" "webclient_appcds_jvmargs" $webclient_appcds_jvmargs
+      Set-SubVal "appcds" "webclient_appcds_archive" $webclient_appcds_archive
+   }
+   Set-Val "webclient_memory" $webclient_memory
+}
+
+# UTF-8 without a BOM: the file is read back by json_template.ps1 and, on a shared source tree, by
+# the jq of json_template.sh, which does not accept a BOM.
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+[System.IO.File]::WriteAllText((Join-Path $cwd $JsonFile),
+                               ($json | ConvertTo-Json -Depth 10),
+                               $utf8NoBom)
+
+Write-Host "Wrote $JsonFile"
+exit 0

=== modified file 'prepare_template.cmd'
--- old/prepare_template.cmd	2023-09-01 00:55:00 +0000
+++ new/prepare_template.cmd	2026-08-07 14:04:48 +0000
@@ -1,352 +1,16 @@
 @echo off
-SETLOCAL ENABLEDELAYEDEXPANSION
-
-set VERBOSE=1
-set ANALYTICS=0
-
-set argCount=0
-for %%x in (%*) do (
-   set /A argCount=!argCount!+1
-   set "argVec[!argCount!]=%%~x"
-)
-set /a i=1
-:while1
-   set carg=!argVec[%i%]!
-
-   if "%carg%" equ "-a" (
-      set ANALYTICS=1
-   ) else if "%carg%" equ "-n" (
-      set VERBOSE=0
-   )
-   set /a i=%i%+1
-   if %i% leq %argCount% ( goto :while1 )
-
-set appname=
-
-:: 1. conversion inputs
-:: [include-spec] *.[fhi] - default
-set include_spec=*.[fhi]
-:: [program-spec] *.[pPwW] - default
-set program_spec=*.[pPwW]
-:: [pkgroot] com.goldencode.hotel default: test.[appname]
-set pkgroot=
-:: [pkgrootfolder] com/goldencode/hotel
-::   automated from [pkgroot] - always use "/"
-set pkgrootfolder=
-:: [propath]
-::   p2j.cfg.linux:   "${P2J_HOME}:${P2J_HOME}/abl"
-::   p2j.cfg.windows: "${P2J_HOME};${P2J_HOME}\abl"
-::   directory.xml: ".: " -> with ": " separator, from this it computes the linux/windows versions
-set propath=.:
-set propathsep=:
-set propathcfg=
-set admversion=
-
-:: 2. database inputs:
-:: [dbnames] hotel1,hotel2
-set dbnames=
-:: [dbadmin] fwd_admin
-set dbadmin=
-:: [dbadminpass] admin
-set dbadminpass=
-:: [dbtype] h2
-set dbtype=h2
-:: [dbhost] localhost
-set dbhost=localhost
-:: [dbport] 5433
-set dbport=5433
-:: [dbuser] fwd_user
-set dbuser=
-:: [dbuserpass] user
-set dbuserpass=
-
-:: 3. runtime setup 
-:: [spawner-path] /opt/spawner/spawn default: $PWD/deploy/spawner/spawn
-:: automated
-set spawner_path=
-:: [client-start-dir] /home/ca/workspace/hotel/deploy/client default: $PWD/deploy/client
-:: automated
-set client_start_dir=
-:: [p2j-entry] com.goldencode.hotel.Login
-set p2j_entry=
-:: [dateFormat] mdy
-set dateFormat=mdy
-:: [numberGroupSep] ,
-set numberGroupSep=,
-:: [numberDecimalSep] .
-set numberDecimalSep=.
-:: [path-separator] :
-set path_separator=:
-:: [file-separator] /
-set file_separator=/
-:: [case-sensitive] TRUE
-set case_sensitive=TRUE
-
-set /p appname= "Enter your application name (only letters or digits, first character a letter): "
-set pkgroot=test.%appname%
-
-:: 1. conversion inputs
-if %VERBOSE%==1 echo 1. Enter conversion configurations for your project
-set /p include_spec= "Edit the regex which matches all your include files (enter for default '%include_spec%'): "
-set /p program_spec= "Edit the regex which matches all your program files (enter for default '%program_spec%'): "
-if "%ANALYTICS%" equ "0" (
-   set /p pkgroot= "Edit the root package name - Java format (enter for default '%pkgroot%'): "
-)
-:: pkgrootfolder - automated
-set /p propath= "Enter the PROPATH from your progress.ini file (enter for default '%propath%'): "
-set /p admversion= "Enter the ADM version (ADM2.2 or ADM1.1): "
- 
-:: 2. database inputs
-if %VERBOSE%==1 echo 2. Enter database configuration for your project
-set /p dbnames= "Enter the legacy database name(s), separated by a comma: "
-
-if "%ANALYTICS%" equ "0" (
-   set /p dbtype= "Enter the database type (h2 or postgresql): "
-   if "%dbtype%" equ "postgresql" (
-      set /p dbhost= "Edit the PostgreSQL host name for your imported database: "
-      set /p dbport= "Edit the PostgreSQL port for your imported database: "
-   )
-   set /p dbadmin= "Enter the SQL admin for your imported database: "
-   set /p dbadminpass= "Enter the SQL admin password for your imported database: "
-   set /p dbuser= "Enter the SQL user for your imported database: "
-   set /p dbuserpass= "Enter the SQL user password for your imported database: "
-   :: 3. runtime setup
-   if %VERBOSE%==1 echo 3. Enter runtime configuration for your project
-   set /p dateFormat= "Enter the date format from your progress.ini (enter for default '%dateFormat%'): "
-   set /p numberGroupSep= "Enter the number group separator from your progress.ini (enter for default '%numberGroupSep%'): "
-   set /p numberDecimalSep= "Enter the number decimal separator from your progress.ini (enter for default '%numberDecimalSep%'): "
-   set /p p2j_entry= "Enter the full class name associated with the entry point for your application: "
-)
-
-:: build automated values
-set pkgrootfolder=%pkgroot:.=/%
-set spawner_path=%cd%\deploy\spawner\spawn.exe
-set client_start_dir=%cd%\deploy\client
-
-set path_separator=;
-set file_separator=\
-set case_sensitive=FALSE
-set unix_escapes=FALSE
-set propathsep=;
-set user=%USERNAME%
-
-set propathcfg=${P2J_HOME}:${P2J_HOME}/abl
-set propathentries=%propath%
-set searchpath=.
-:nextSplit
-for /F "tokens=1* delims=%propathsep%" %%a in ("%propathentries%") do (
-   if "%%a" neq "." (
-      set pcfgpentry=%%a
-      set pcfgpentry=!pcfgpentry:\=/!
-      set propathcfg=%propathcfg%:${P2J_HOME}/abl/!pcfgpentry!
-
-      set ppentry=%%a
-      set ppentry=!ppentry:/=\!
-      set searchpath=%searchpath%;!ppentry!
-   )
-   set propathentries=%%b
-)
-if defined propathentries goto nextSplit
-
-set dbh2=false
-set dbpostgresql=false
-set dbmariadb=false
-
-if "%dbtype%" equ "h2" (
-   set dbh2=true
-)
-if "%dbtype%" equ "postgresql" (
-   set dbpostgresql=true
-)
-if "%dbtype%" equ "mariadb" (
-   set dbmariadb=true
-)
-
-set dbnamesx=%dbnames%
-for /F "tokens=1* delims=," %%a in ("%dbnamesx%") do (
-   set dbnamesx=%%b
-   set dbname=%%a
-   goto :donefor
-)
-:donefor
-
-SETLOCAL DISABLEDELAYEDEXPANSION
-del /f build.properties
-for /f "tokens=* delims=" %%x in (build.properties.template) do (
-   set b=%%x
-   SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
-   set b=!b:[appname]=%appname%!
-   set b=!b:[program-spec]=%program_spec%!
-   set b=!b:[dbh2]=%dbh2%!
-   set b=!b:[dbpostgresql]=%dbpostgresql%!
-   set b=!b:[dbmariadb]=%dbmariadb%!
-   set b=!b:[dbhost]=%dbhost%!
-   set b=!b:[dbport]=%dbport%!
-   set b=!b:[dbnames]=%dbnames%!
-   set b=!b:[dbuser]=%dbuser%!
-   set b=!b:[dbuserpass]=%dbuserpass%!
-   set b=!b:[dbadmin]=%dbadmin%!
-   set b=!b:[dbadminpass]=%dbadminpass%!
-   set b=!b:[pkgrootfolder]=%pkgrootfolder%!
-   set b=!b:[admversion]=%admversion%!
-   echo.!b!>> build.properties
-   ENDLOCAL
-)
-ENDLOCAL
-
-SETLOCAL DISABLEDELAYEDEXPANSION 
-del /f deploy\server\directory.xml
-for /f "tokens=* delims=" %%x in (deploy\server\directory.xml.template) do (
-   set b=%%x
-   SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
-   set b=!b:[spawner-path]=%spawner_path%!
-   set b=!b:[client-start-dir]=%client_start_dir%!
-   set b=!b:[dateFormat]=%dateFormat%!
-   set b=!b:[numberGroupSep]=%numberGroupSep%!
-   set b=!b:[numberDecimalSep]=%numberDecimalSep%!
-   set b=!b:[p2j-entry]=%p2j_entry%!
-   set b=!b:[pkgroot]=%pkgroot%!
-   set b=!b:[propath]=%propath%!
-   set b=!b:[search-path]=%searchpath%!
-   set b=!b:[path-separator]=%path_separator%!
-   set b=!b:[file-separator]=%file_separator%!
-   set b=!b:[case-sensitive]=%case_sensitive%!
-   set b=!b:[dbname]=%dbname%!
-   set b=!b:[dbuser]=%dbuser%!
-   set b=!b:[dbuserpass]=%dbuserpass%!
-   set b=!b:[dbadmin]=%dbadmin%!
-   set b=!b:[dbadminpass]=%dbadminpass%!
-   set b=!b:[os-user]=%user%!
-   echo.!b!>> deploy\server\directory.xml
-   ENDLOCAL
-)
-ENDLOCAL
-
-set dbnamesx=%dbnames%
-set "dbname="
-set dirfile=deploy\server\directory.xml
-set dbtemp=deploy\server\directory_db_%dbtype%.xml.template
-:nextdb1
-SETLOCAL DISABLEDELAYEDEXPANSION
-for /F "tokens=1* delims=," %%a in ("%dbnamesx%") do (
-   SETLOCAL ENABLEDELAYEDEXPANSION
-
-   set dbname=%%a
-   set dbfile=deploy\server\directory_db.xml.!dbname!
-
-   call :builddbxml !dbname! !dbfile!
-
-   java -Xmx256m -classpath p2j\build\lib\p2j.jar com.goldencode.p2j.directory.DirectoryCopy copy !dbfile! /server/standard/database/ %dirfile% /server/standard/database/
-
-   del /f !dbfile!
-   ENDLOCAL
-
-   set dbnamesx=%%b
-)
-if defined dbnamesx goto nextdb1
-ENDLOCAL
-
-del /f cfg\p2j.cfg.xml
-
-SETLOCAL DISABLEDELAYEDEXPANSION
-for /f "tokens=* delims=" %%x in (cfg\p2j.cfg.xml.template) do (
-   set b=%%x
-   SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
-   set b=!b:[propath]=%propathcfg%!
-   set b=!b:[include-spec]=%include_spec%!
-   set b=!b:[pkgroot]=%pkgroot%!
-   set b=!b:[path-separator]=%path_separator%!
-   set b=!b:[file-separator]=%file_separator%!
-   set b=!b:[case-sensitive]=%case_sensitive%!
-   set b=!b:[unix-escapes]=%unix_escapes%!
-   echo.!b!>> cfg\p2j.cfg.xml
-   ENDLOCAL
-)
-ENDLOCAL
-
-set dbnamesx=%dbnames%
-set dbnamespaces=
-
-SETLOCAL ENABLEDELAYEDEXPANSION
-:nextdb2
-for /F "tokens=1* delims=," %%a in ("%dbnamesx%") do (
-   set dbname=%%a
-
-   call :cfgns !dbname!
-
-   for /f "tokens=* delims=" %%x in (cfg\p2j_!dbname!.namespace.xml) do (
-      set dbnamespaces=!dbnamespaces! %%x
-   )
-   del /f cfg\p2j_!dbname!.namespace.xml
-
-   set dbnamesx=%%b
-)
-
-if defined dbnamesx goto nextdb2
-
-copy cfg\p2j.cfg.xml cfg\p2j.cfg.xml.dbs
-del /f cfg\p2j.cfg.xml
-SETLOCAL DISABLEDELAYEDEXPANSION
-for /f "tokens=* delims=" %%x in (cfg\p2j.cfg.xml.dbs) do (
-   set y=%%x
-
-   SETLOCAL ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
-   if "!y:~-14!" equ "[dbnamespaces]" (
-      echo.!dbnamespaces!>> cfg\p2j.cfg.xml
-   ) else (
-      echo.!y!>> cfg\p2j.cfg.xml
-   )
-   ENDLOCAL
-)
-ENDLOCAL
-
-del /f cfg\p2j.cfg.xml.dbs
-
-ENDLOCAL
-
-echo.Class-Path: %appname%.jar> manifest\%appname%.mf
-
-if "%appname%" neq "hotel" (
-   del /f hotel.bat hotel.ini hotel.input.linux hotel.input.windows hotel.pf prepare_hotel.cmd ^
-          prepare_hotel.sh data\hotel.df manifest\hotel.mf src\text-metrics.xml start_hotel_abl.cmd
-   rmdir /s /q data\dump\hotel
-   rmdir /s /q  abldb\
-   rmdir /s /q  abl\
-   mkdir abl
-   del /f cfg\p2j.cfg.xml.template deploy\server\directory.xml.template build.properties.template ^
-      components-4gl-fwd.sh components-fwd-4gl.sh components-4gl-fwd.cmd components-fwd-4gl.cmd
-)
-
-exit /b
-
-:cfgns
-   SETLOCAL DISABLEDELAYEDEXPANSION
-   for /f "tokens=* delims=" %%x in (cfg\p2j.namespace.xml.template) do (
-      set y=%%x
-
-      SETLOCAL ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
-      set y=!y:[dbname]=%1!
-      echo.!y!>> cfg\p2j_%1.namespace.xml
-      ENDLOCAL
-   )
-   ENDLOCAL
-   exit /b
-
-:builddbxml
-   SETLOCAL DISABLEDELAYEDEXPANSION
-   for /f "tokens=* delims=" %%x in (%dbtemp%) do (
-      SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
-
-      set y=%%x
-      set y=!y:[dbhost]=%dbhost%!
-      set y=!y:[dbport]=%dbport%!
-      set y=!y:[dbname]=%1!
-      set y=!y:[dbuser]=%dbuser%!
-      set y=!y:[dbuserpass]=%dbuserpass%!
-      set y=!y:[dbadmin]=%dbadmin%!
-      set y=!y:[dbadminpass]=%dbadminpass%!
-      echo.!y!>> %2
-      ENDLOCAL
-   )
-   ENDLOCAL
-   exit /b
+::
+:: Wrapper for prepare_template.ps1, the counterpart of prepare_template.sh: prepare_json.ps1 to
+:: collect the answers, then json_template.ps1 to render the configuration from them.
+::
+:: This replaces the batch reimplementation which used to live here.  That one predated the json
+:: pipeline: it still substituted the retired [marker] syntax, so against the current templates it
+:: matched nothing and copied them through with every marker left intact.
+::
+:: Run this from the project root, the scripts resolve their templates relative to the current
+:: directory exactly as the shell versions do.
+::
+set scriptdir=%~dp0
+
+call "%scriptdir%run_powershell.cmd" "%scriptdir%prepare_template.ps1" %*
+exit /b %ERRORLEVEL%

=== added file 'prepare_template.ps1'
--- old/prepare_template.ps1	1970-01-01 00:00:00 +0000
+++ new/prepare_template.ps1	2026-08-07 14:04:48 +0000
@@ -0,0 +1,36 @@
+#
+# The Windows counterpart of prepare_template.sh: collects the answers into prepare_template.json,
+# then renders the configuration from it.  The two stages are separate scripts so the second one can
+# be re-run against an edited json without answering everything again.
+#
+param (
+   [switch]$n,
+   [switch]$a
+)
+
+Set-PSDebug -Trace 0
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$here = Split-Path -Path $MyInvocation.MyCommand.Path -Parent
+
+# The interpreter running this script, not a hardcoded "pwsh": PowerShell 7 is not necessarily
+# installed, run_powershell.cmd launches the Windows PowerShell 5.1 instead.
+$psHost = (Get-Process -Id $PID).Path
+
+# Each stage runs as its own process, as the "./prepare_json.sh" of prepare_template.sh does.  The
+# child inherits this process' stdin, so the redirected *.input.* file still reaches prepare_json,
+# and an "exit" of a stage cannot take this script down with it.
+$forward = @()
+if ($n) { $forward += '-n' }
+if ($a) { $forward += '-a' }
+
+& $psHost -NoProfile -ExecutionPolicy Bypass -File (Join-Path $here 'prepare_json.ps1') @forward
+if ($LASTEXITCODE -ne 0)
+{
+   exit $LASTEXITCODE
+}
+
+# -o so a re-run replaces the directory.xml, which is what prepare_template.sh passes too.
+& $psHost -NoProfile -ExecutionPolicy Bypass -File (Join-Path $here 'json_template.ps1') -o @forward
+exit $LASTEXITCODE

=== added file 'run_powershell.cmd'
--- old/run_powershell.cmd	1970-01-01 00:00:00 +0000
+++ new/run_powershell.cmd	2026-08-07 14:04:48 +0000
@@ -0,0 +1,50 @@
+@echo off
+::
+:: Runs a PowerShell script with whichever PowerShell is installed: PowerShell Core (pwsh.exe) when it
+:: is on the PATH, otherwise the Windows PowerShell 5.1 (powershell.exe) which is preinstalled with
+:: Windows 10.  This keeps the callers from having to hardcode one of the two.
+::
+:: Usage: run_powershell.cmd <script.ps1> [ <argument> ... ]
+::
+:: The exit code of the script is passed on, so a caller can check ERRORLEVEL as usual.  Arguments are
+:: forwarded quoted, which covers the paths with spaces; a path holding one of the cmd.exe special
+:: characters (& ^ %%) still has to be avoided.
+::
+setlocal
+
+if "%~1" == "" (
+   echo Usage: %~nx0 ^<script.ps1^> [ ^<argument^> ... ]
+   exit /b 1
+)
+
+set "psscript=%~1"
+if not exist "%psscript%" (
+   echo ERROR: %psscript% not found.
+   exit /b 1
+)
+
+:: Look both interpreters up on the PATH, PowerShell Core first.  %%~$PATH:I expands to the full path
+:: of the file when it is found there, and to an empty string when it is not.
+set "psexe="
+for %%I in (pwsh.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+if not defined psexe (
+   for %%I in (powershell.exe) do if not "%%~$PATH:I" == "" set "psexe=%%~$PATH:I"
+)
+if not defined psexe (
+   echo ERROR: neither pwsh.exe nor powershell.exe was found on the PATH.
+   exit /b 1
+)
+
+:: Drop the script name, all the remaining arguments belong to the script
+shift
+
+set "psargs="
+:collect_args
+if "%~1" == "" goto run_script
+set psargs=%psargs% "%~1"
+shift
+goto collect_args
+
+:run_script
+"%psexe%" -NoProfile -ExecutionPolicy Bypass -File "%psscript%"%psargs%
+exit /b %ERRORLEVEL%

